├── .gitignore ├── MANIFEST.in ├── README.rst ├── setup.py └── skosxl ├── __init__.py ├── admin.py ├── fixtures └── initial_data.json ├── locale └── fr │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── models.py ├── static ├── d3.js ├── d3.layout.js ├── d3.tree.css └── d3.tree.js ├── templates ├── admin │ └── autocomplete │ │ ├── fk_searchinput.html │ │ ├── inline_searchinput.html │ │ └── nolookups_foreignkey_searchinput.html ├── admin_concept_change.html ├── admin_concept_list.html ├── concept_detail.html ├── scheme_detail.html └── tag_detail.html ├── urls.py ├── utils ├── __init__.py ├── autocomplete_admin.py └── cors.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # File types # 2 | ############## 3 | *.pyc 4 | *.tmproj 5 | *.komodoproject 6 | #*.json 7 | *.db 8 | *.swp 9 | *.bak 10 | @@* 11 | 12 | # Directories # 13 | ############### 14 | 15 | # Specific files # 16 | ################## 17 | #/settings.py 18 | /base.db 19 | /activate_env.bat 20 | /deactivate_env.bat 21 | 22 | # OS generated files # 23 | ###################### 24 | .DS_Store 25 | Icon? 26 | Thumbs.db 27 | 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include skosxl/locale *.po *.mo 2 | recursive-include skosxl/templates *.html *.js 3 | recursive-include skosxl/static *.js *.css *.png *.jpg -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django-SKOSXL 2 | =============================================== 3 | 4 | 5 | What is django-skosxl good for? 6 | ------------------------------------ 7 | django-coop is a set of several apps to make different websites cooperate. It is based on Django. 8 | 9 | Websites often need to categorize things, but when time comes for agregating all the data and gather them by tags or categories, we often have face the fact that we have differents systems. Term reconciliation is a hard task. 10 | 11 | The W3C SKOS specification allows us to define schemes of related Concepts, and the XL extension to the SKOS RDF vocabulary allow us to specify different labels in different languages, so you can have a "preferred label" for english, for french, and alternative or hidden labels too. 12 | 13 | The goal is here is to allow free tagging of objects (folksonomy), but then regroup similar terms into common concepts. And finally link the concepts between them, using the SKOS relations (broader, narrower, related). 14 | 15 | The label management part of this app is built around django-taggit, in order to ease end-user input of the labels. 16 | The django admin site then allows an administrator to link labels to concepts and model its SKOS concept tree. 17 | 18 | 19 | License 20 | ======= 21 | 22 | django-skosxl uses the same license as Django (BSD-like). 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | try: 5 | from setuptools import setup, find_packages 6 | except ImportError: 7 | import ez_setup 8 | ez_setup.use_setuptools() 9 | from setuptools import setup, find_packages 10 | 11 | VERSION = __import__('skosxl').__version__ 12 | 13 | import os 14 | def read(fname): 15 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 16 | 17 | setup( 18 | name='django-skosxl', 19 | version = VERSION, 20 | description='Pluggable django application for managing a SKOS-XL Thesaurus, based on a tag folksonomy', 21 | packages=['skosxl','skosxl.utils'], 22 | include_package_data=True, 23 | author='Dominique Guardiola', 24 | author_email='dguardiola@quinode.fr', 25 | license='BSD', 26 | long_description=read('README.rst'), 27 | #download_url = "https://github.com/quinode/django-skosxl/tarball/%s" % (VERSION), 28 | #download_url='git://github.com/quinode/django-skosxl.git', 29 | zip_safe=False, 30 | install_requires = ['django-taggit>=0.9.3', 31 | 'SPARQLWrapper>=1.5.0', 32 | 'django-extensions>=0.7.1', 33 | 'django-admin-tools>=0.4.1', 34 | 'django-extended-choices' 35 | ], 36 | dependency_links = [ 37 | 'https://github.com/flupke/django-taggit-templatetags.git#egg=taggit_templatetags', 38 | 'https://github.com/twidi/django-extended-choices.git#egg=extended_choices', 39 | ], 40 | ) 41 | 42 | -------------------------------------------------------------------------------- /skosxl/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | VERSION = (0, 2) 4 | 5 | def get_version(): 6 | return '%s.%s' % (VERSION[0], VERSION[1]) 7 | 8 | __version__ = get_version() -------------------------------------------------------------------------------- /skosxl/admin.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | from django.contrib import admin 3 | from skosxl.models import * 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from skosxl.utils.autocomplete_admin import FkAutocompleteAdmin, InlineAutocompleteAdmin, NoLookupsForeignKeyAutocompleteAdmin 7 | 8 | 9 | # class LabelInline(InlineAutocompleteAdmin): 10 | # model = LabelProperty 11 | # fields = ('label','label_type',) 12 | # related_search_fields = {'label' : ('name','slug')} 13 | # extra=1 14 | # 15 | 16 | class LabelInline(InlineAutocompleteAdmin): 17 | model = Label 18 | readonly_fields = ('created',) 19 | fields = ('language','label_type','label_text','created') 20 | related_search_fields = {'label' : ('label_text',)} 21 | extra=1 22 | 23 | class NotationInline(InlineAutocompleteAdmin): 24 | model = Notation 25 | # readonly_fields = ('slug','created') 26 | fields = ('code','namespace') 27 | # related_search_fields = {'label' : ('name','slug')} 28 | extra=1 29 | 30 | 31 | class SKOSMappingInline(admin.TabularInline): 32 | model = MapRelation 33 | fk_name = 'origin_concept' 34 | fields = ('match_type','uri') 35 | # related_search_fields = {'target_concept' : ('labels__name','definition')} 36 | extra=1 37 | 38 | class RelInline(InlineAutocompleteAdmin): 39 | model = SemRelation 40 | fk_name = 'origin_concept' 41 | fields = ('rel_type', 'target_concept') 42 | related_search_fields = {'target_concept' : ('labels__name','definition')} 43 | extra = 1 44 | 45 | def create_action(scheme): 46 | fun = lambda modeladmin, request, queryset: queryset.update(scheme=scheme) 47 | name = "moveto_%s" % (scheme.slug,) 48 | return (name, (fun, name, _(u'Make selected concept part of the "%s" scheme') % (scheme,))) 49 | 50 | 51 | class ConceptAdmin(FkAutocompleteAdmin): 52 | readonly_fields = ('created','modified') 53 | search_fields = ['pref_label','slug','definition'] 54 | list_display = ('pref_label','status','scheme','top_concept','created') 55 | list_editable = ('status','scheme','top_concept') 56 | 57 | list_filter = ('scheme','status') 58 | change_form_template = 'admin_concept_change.html' 59 | change_list_template = 'admin_concept_list.html' 60 | # def change_view(self, request, object_id, extra_context=None): 61 | # from SPARQLWrapper import SPARQLWrapper,JSON, XML 62 | # #import pdb; pdb.set_trace() 63 | # obj = Concept.objects.get(id=object_id) 64 | # my_context = {'lists' : []} 65 | # endpoints = ( 66 | # #('AGROVOC','http://202.73.13.50:55824/catalogs/performance/repositories/agrovoc'), 67 | # #('ISIDORE','http://www.rechercheisidore.fr/sparql?format=application/sparql-results+json'), 68 | # ('GEMET','http://cr.eionet.europa.eu/sparql'), 69 | # ) 70 | # for endpoint in endpoints : 71 | # try: 72 | # sparql = SPARQLWrapper(endpoint[1]) 73 | # sparql.setQuery(u""" 74 | # PREFIX rdfs: 75 | # PREFIX rdf: 76 | # PREFIX skos: 77 | # SELECT ?label ?uri WHERE { 78 | # ?uri a skos:Concept . 79 | # ?uri skos:prefLabel ?label . 80 | # FILTER(regex(str(?label),""" + u'"'+obj.pref_label+u'"' + u""","i")) 81 | # FILTER( lang(?label) = "fr" ) 82 | # } 83 | # """) 84 | # sparql.setReturnFormat(JSON) 85 | # results = sparql.query().convert() 86 | # # for result in results["results"]["bindings"]: 87 | # # print result["label"]["value"] 88 | # 89 | # my_context['lists'].append({'name': endpoint[0],'items':results["results"]["bindings"]}) 90 | # except Exception,e : 91 | # print "Caught:", e 92 | # 93 | # return super(ConceptAdmin, self).change_view(request, object_id, extra_context=my_context) 94 | 95 | def changelist_view(self, request, extra_context=None): 96 | try : 97 | scheme_id = int(request.GET['scheme__id__exact']) 98 | except KeyError : 99 | scheme_id = 1 # FIXME: if no scheme filter is called, get the first (or "General") : a fixture to create one ? 100 | return super(ConceptAdmin, self).changelist_view(request, 101 | extra_context={'scheme_id':scheme_id}) 102 | 103 | fieldsets = ( (_(u'Scheme'), {'fields':('scheme','pref_label','top_concept')}), 104 | (_(u'Meta-data'), 105 | {'fields':(('definition','changenote'),'created','modified'), 106 | 'classes':('collapse',)}), 107 | ) 108 | inlines = [ NotationInline, LabelInline, RelInline, SKOSMappingInline] 109 | def get_actions(self, request): 110 | return dict(create_action(s) for s in Scheme.objects.all()) 111 | 112 | admin.site.register(Concept, ConceptAdmin) 113 | 114 | 115 | 116 | def create_concept_command(modeladmin, request, queryset): 117 | for label in queryset: 118 | label.create_concept_from_label() 119 | create_concept_command.short_description = _(u"Create concept(s) from selected label(s)") 120 | 121 | 122 | class ConceptInline(InlineAutocompleteAdmin): 123 | model = Concept 124 | readonly_fields = ('pref_label',) 125 | fields = ('pref_label','top_concept','status') 126 | # list_display = ('pref_label',) 127 | related_search_fields = {'concept' : ('prefLabel','definition')} 128 | extra = 0 129 | 130 | class LabelAdmin(FkAutocompleteAdmin): 131 | list_display = ('label_text','label_type','concept') 132 | fields = ('label_text','language','label_type','concept') 133 | related_search_fields = {'concept' : ('pref_label','definition')} 134 | # actions = [create_concept_command] 135 | #list_editable = ('name','slug') 136 | search_fields = ['label_text',] 137 | 138 | admin.site.register(Label, LabelAdmin) 139 | 140 | 141 | class SchemeAdmin(FkAutocompleteAdmin): 142 | readonly_fields = ('created','modified') 143 | inlines = [ ConceptInline, ] 144 | 145 | admin.site.register(Scheme, SchemeAdmin) 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /skosxl/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ { 2 | "pk" : null, 3 | "model" : "rdf_io.objectmapping", 4 | "fields" : { 5 | "name" : "skos:Concept default", 6 | "id_attr" : "term", 7 | "target_uri_expr" : "scheme.uri", 8 | "obj_type" : [["skos:Concept"]], 9 | "content_type" : ["skosxl", "concept"] 10 | } 11 | }, { 12 | "pk" : null, 13 | "model" : "rdf_io.attributemapping", 14 | "fields" : { 15 | "scope" : ["skos:Concept default"], 16 | "predicate" : "skos:definition", 17 | "is_resource" : false, 18 | "attr" : "definition" 19 | } 20 | }, { 21 | "pk" : null, 22 | "model" : "rdf_io.attributemapping", 23 | "fields" : { 24 | "scope" : ["skos:Concept default"], 25 | "predicate" : "skos:inScheme", 26 | "is_resource" : true, 27 | "attr" : "scheme.uri" 28 | } 29 | }, { 30 | "pk" : null, 31 | "model" : "rdf_io.attributemapping", 32 | "fields" : { 33 | "scope" : ["skos:Concept default"], 34 | "predicate" : "skos:prefLabel", 35 | "is_resource" : false, 36 | "attr" : "label[label_type=0].label_text@language" 37 | } 38 | }, 39 | { 40 | "pk" : null, 41 | "model" : "rdf_io.objectmapping", 42 | "fields" : { 43 | "name" : "skos:ConceptScheme default", 44 | "id_attr" : "uri", 45 | "target_uri_expr" : "uri", 46 | "obj_type" : [["skos:ConceptScheme"]], 47 | "content_type" : ["skosxl", "scheme"] 48 | } 49 | }, { 50 | "pk" : null, 51 | "model" : "rdf_io.attributemapping", 52 | "fields" : { 53 | "scope" : ["skos:ConceptScheme default"], 54 | "predicate" : "skos:definition", 55 | "is_resource" : false, 56 | "attr" : "definition" 57 | } 58 | }, { 59 | "pk" : null, 60 | "model" : "rdf_io.attributemapping", 61 | "fields" : { 62 | "scope" : ["skos:ConceptScheme default"], 63 | "predicate" : "skos:prefLabel", 64 | "is_resource" : false, 65 | "attr" : "pref_label" 66 | } 67 | }, 68 | { 69 | "pk" : null, 70 | "model" : "rdf_io.attributemapping", 71 | "fields" : { 72 | "scope" : ["skos:ConceptScheme default"], 73 | "predicate" : "schememeta.metaprop", 74 | "is_resource" : false, 75 | "attr" : "schememeta.value" 76 | } 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /skosxl/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-rdf-tools/django-skosxl/5636e0474cc6dcfe48cc6187dcbdfa52ab252d03/skosxl/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /skosxl/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-skosxl\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2012-01-07 14:51+0000\n" 11 | "PO-Revision-Date: 2012-01-07 15:51+0100\n" 12 | "Last-Translator: Dominique Guardiola \n" 13 | "Language-Team: Quinode \n" 14 | "Language: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 19 | "X-Poedit-Language: French\n" 20 | "X-Poedit-Country: FRANCE\n" 21 | 22 | #: admin.py:38 23 | #, python-format 24 | msgid "Make selected concept part of the \"%s\" scheme" 25 | msgstr "Rattacher les concepts sélectionnés au schéma \"%s\"" 26 | 27 | #: admin.py:48 28 | msgid "Scheme" 29 | msgstr "Schéma" 30 | 31 | #: admin.py:49 32 | msgid "Meta-data" 33 | msgstr "Méta-données" 34 | 35 | #: admin.py:62 36 | msgid "Create concept(s) from selected label(s)" 37 | msgstr "Créer des concepts à partir des tags sélectionnés" 38 | 39 | #: models.py:20 40 | msgid "preferred" 41 | msgstr "préféré" 42 | 43 | #: models.py:21 44 | msgid "alternative" 45 | msgstr "alternatif" 46 | 47 | #: models.py:22 48 | msgid "hidden" 49 | msgstr "masqué" 50 | 51 | #: models.py:28 52 | msgid "has a broader concept" 53 | msgstr "a pour concept plus large" 54 | 55 | #: models.py:29 56 | msgid "has a narrower concept" 57 | msgstr "a comme concept plus précis" 58 | 59 | #: models.py:30 60 | msgid "has a related concept" 61 | msgstr "a une relation avec" 62 | 63 | #: models.py:42 64 | msgid "matches exactly" 65 | msgstr "correspond exactement à" 66 | 67 | #: models.py:43 68 | msgid "matches closely" 69 | msgstr "est assez proche de" 70 | 71 | #: models.py:44 72 | msgid "has a broader match" 73 | msgstr "a une signification plus large que" 74 | 75 | #: models.py:45 76 | msgid "has a narrower match" 77 | msgstr "a une signification plus précise que" 78 | 79 | #: models.py:46 80 | msgid "has a related match" 81 | msgstr "e une relation avec" 82 | 83 | #: models.py:51 84 | msgid "French" 85 | msgstr "Français" 86 | 87 | #: models.py:52 88 | msgid "English" 89 | msgstr "Anglais" 90 | 91 | #: models.py:53 92 | msgid "Spanish" 93 | msgstr "Espagnol" 94 | 95 | #: models.py:54 96 | msgid "Italian" 97 | msgstr "Italien" 98 | 99 | #: models.py:55 100 | msgid "Portuguese" 101 | msgstr "Portugais" 102 | 103 | #: models.py:61 104 | msgid "Active" 105 | msgstr "Actif" 106 | 107 | #: models.py:62 108 | msgid "Draft" 109 | msgstr "Brouillon" 110 | 111 | #: models.py:63 112 | msgid "Double" 113 | msgstr "Doublon" 114 | 115 | #: models.py:64 116 | msgid "Dispute" 117 | msgstr "Désaccord" 118 | 119 | #: models.py:65 120 | msgid "Not classified" 121 | msgstr "pas trié" 122 | 123 | #: models.py:71 124 | #: models.py:94 125 | msgid "preferred label" 126 | msgstr "libellé préféré" 127 | 128 | #: models.py:73 129 | #: models.py:91 130 | #: models.py:92 131 | #: models.py:125 132 | msgid "main URI" 133 | msgstr "URI" 134 | 135 | #: models.py:74 136 | #: models.py:85 137 | #: models.py:126 138 | msgid "created" 139 | msgstr "date de création" 140 | 141 | #: models.py:75 142 | #: models.py:86 143 | #: models.py:127 144 | msgid "modified" 145 | msgstr "date de modification" 146 | 147 | #: models.py:81 148 | msgid "definition" 149 | msgstr "définition" 150 | 151 | #: models.py:84 152 | msgid "change note" 153 | msgstr "historique" 154 | 155 | #: models.py:87 156 | msgid "review status" 157 | msgstr "évaluation" 158 | 159 | #: models.py:90 160 | #: models.py:123 161 | msgid "django user" 162 | msgstr "utilisateur Django" 163 | 164 | #: models.py:96 165 | msgid "is top concept" 166 | msgstr "concept de niveau 1" 167 | 168 | #: models.py:99 169 | msgid "semantic relations" 170 | msgstr "relations sémantiques" 171 | 172 | #: models.py:122 173 | msgid "language" 174 | msgstr "langue" 175 | 176 | #: models.py:124 177 | msgid "author URI" 178 | msgstr "URI auteur" 179 | 180 | #: models.py:128 181 | msgid "main concept" 182 | msgstr "concept" 183 | 184 | #: models.py:129 185 | #: models.py:154 186 | msgid "label type" 187 | msgstr "type de libellé" 188 | 189 | #: models.py:137 190 | msgid "Created from tag \"" 191 | msgstr "Créé à partir du tag \"" 192 | 193 | #: models.py:152 194 | msgid "label" 195 | msgstr "libellé" 196 | 197 | #: models.py:153 198 | msgid "concept" 199 | msgstr "concept" 200 | 201 | #: models.py:158 202 | msgid "Label property" 203 | msgstr "Propriété du libellé" 204 | 205 | #: models.py:159 206 | msgid "Label properties" 207 | msgstr "Propriétés de libellés" 208 | 209 | #: models.py:171 210 | msgid "There can be only one preferred label by language" 211 | msgstr "Il ne peut y avoir d'un seul libellé préféré par concept et par langue." 212 | 213 | #: models.py:190 214 | msgid "Origin" 215 | msgstr "Origine" 216 | 217 | #: models.py:191 218 | msgid "Target" 219 | msgstr "Cible" 220 | 221 | #: models.py:192 222 | msgid "Type of semantic relation" 223 | msgstr "Type de relation sémantique" 224 | 225 | #: models.py:196 226 | #: models.py:197 227 | msgid "Semantic relations" 228 | msgstr "Relations sémantiques" 229 | 230 | #: templates/admin_concept_change.html:19 231 | msgid "Home" 232 | msgstr "Accueil" 233 | 234 | #: templates/admin_concept_change.html:22 235 | msgid "Add" 236 | msgstr "Ajouter" 237 | 238 | #: templates/admin_concept_change.html:44 239 | msgid "History" 240 | msgstr "Historique" 241 | 242 | #: templates/admin_concept_change.html:45 243 | msgid "View on site" 244 | msgstr "Voir sur le site" 245 | 246 | #: templates/admin_concept_change.html:56 247 | msgid "Please correct the error below." 248 | msgid_plural "Please correct the errors below." 249 | msgstr[0] "Merci de corriger l'erreur suivante." 250 | msgstr[1] "Merci de corriger les erreurs suivantes." 251 | 252 | #~ msgid "Has a broader (transitive) concept" 253 | #~ msgstr "A pour concept plus large (transitif)" 254 | 255 | #~ msgid "Has a narrower (transitive) concept" 256 | #~ msgstr "A pour concept plus précis (transitif)" 257 | 258 | #~ msgid "< missing label >" 259 | #~ msgstr "< label manquant >" 260 | 261 | #~ msgid "Literal Form" 262 | #~ msgstr "Forme littérale" 263 | 264 | #~ msgid "Occurrences" 265 | #~ msgstr "Occurrences" 266 | 267 | #~ msgid "Term" 268 | #~ msgstr "Terme" 269 | 270 | #~ msgid "Terms" 271 | #~ msgstr "Termes" 272 | 273 | #~ msgid "Name" 274 | #~ msgstr "Nom" 275 | 276 | #~ msgid "URL" 277 | #~ msgstr "Adresse URL" 278 | 279 | #~ msgid "SKOS Thesaurus" 280 | #~ msgstr "Thesaurus SKOS" 281 | 282 | #~ msgid "SKOS Thesaurii" 283 | #~ msgstr "Thesaurii SKOS" 284 | 285 | #~ msgid "Type of label" 286 | #~ msgstr "Type de libellé" 287 | 288 | #~ msgid "Labels" 289 | #~ msgstr "Libellés" 290 | 291 | #~ msgid "Local concept to map" 292 | #~ msgstr "Concept local à relier" 293 | 294 | #~ msgid "Concept URI" 295 | #~ msgstr "URI du concept" 296 | 297 | #~ msgid "Type of mapping relation" 298 | #~ msgstr "Type de correspondance" 299 | 300 | #~ msgid "Mapping relation" 301 | #~ msgstr "Correspondance" 302 | 303 | #~ msgid "Mapping relations" 304 | #~ msgstr "Correspondances" 305 | 306 | #~ msgid "Preferred Label" 307 | #~ msgstr "Libellé préféré" 308 | -------------------------------------------------------------------------------- /skosxl/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # Modified from original unmaintained example as part of the django.coop eco-system 4 | # Based on SKOS an SKOS-XL ontologies 5 | # http://www.w3.org/2004/02/skos/core 6 | # http://www.w3.org/2008/05/skos-xl 7 | # 8 | # now synced with a configurable RDF mapping and export module django-rdf-io 9 | 10 | from django.db import models 11 | from django_extensions.db import fields as exfields 12 | from django.utils.translation import ugettext_lazy as _ 13 | from django.utils.translation import ugettext_lazy 14 | from extended_choices import Choices 15 | from django.core.exceptions import ValidationError 16 | from django.core.urlresolvers import reverse 17 | # update this to use customisable setting 18 | # from django.contrib.auth.models import User 19 | from django.conf import settings 20 | 21 | #from taggit.models import TagBase, GenericTaggedItemBase 22 | #from taggit.managers import TaggableManager 23 | from rdf_io.models import Namespace, GenericMetaProp 24 | 25 | import json 26 | 27 | 28 | LABEL_TYPES = Choices( 29 | ('prefLabel', 0, u'preferred'), 30 | ('altLabel', 1, u'alternative'), 31 | ('hiddenLabel', 2, u'hidden'), 32 | ) 33 | 34 | REL_TYPES = Choices( 35 | # ('broaderTransitive', 0, u'Has a broader (transitive) concept'), 36 | # ('narrowerTransitive', 1, u'Has a narrower (transitive) concept'), 37 | ('broader', 0, u'has a broader concept'), 38 | ('narrower', 1, u'has a narrower concept'), 39 | ('related', 2, u'has a related concept'), 40 | ) 41 | 42 | reverse_map = { 43 | # REL_TYPES.narrowerTransitive : REL_TYPES.broaderTransitive, 44 | # REL_TYPES.broaderTransitive : REL_TYPES.narrowerTransitive, 45 | REL_TYPES.narrower : REL_TYPES.broader, 46 | REL_TYPES.broader : REL_TYPES.narrower, 47 | REL_TYPES.related : REL_TYPES.related 48 | } 49 | 50 | MATCH_TYPES = Choices( 51 | ('exactMatch', 0, u'matches exactly'), 52 | ('closeMatch', 1, u'matches closely'), 53 | ('broadMatch', 2, u'has a broader match'), 54 | ('narrowMatch', 3, u'has a narrower match'), 55 | ('relatedMatch', 4, u'has a related match'), 56 | ) 57 | 58 | # TODO - allow these to be defined by the environment - or extended as needed. 59 | LANG_LABELS = ( 60 | ('fr',_(u'French')), 61 | ('de',_(u'German')), 62 | ('en',_(u'English')), 63 | ('es',_(u'Spanish')), 64 | ('it',_(u'Italian')), 65 | ('pt',_(u'Portuguese')) 66 | ) 67 | 68 | DEFAULT_LANG = 'en' 69 | 70 | REVIEW_STATUS = Choices( 71 | ('active', 0, u'Active'), 72 | ('draft', 1, u'Draft'), 73 | ('doubled', 2, u'Double'), 74 | ('dispute', 3, u'Dispute'), 75 | ('todo', 4, u'Not classified'), 76 | ) 77 | 78 | DEFAULT_SCHEME_SLUG = 'general' 79 | 80 | 81 | class SchemeManager(models.Manager): 82 | def get_by_natural_key(self, uri): 83 | return self.get( uri = uri) 84 | 85 | class Scheme(models.Model): 86 | objects = SchemeManager() 87 | 88 | pref_label = models.CharField(_(u'label'),blank=True,max_length=255)#should just be called label 89 | slug = exfields.AutoSlugField(populate_from=('pref_label')) 90 | # URI doesnt need to be a registered Namespace unless you want to use prefix:term expansion for it 91 | uri = models.CharField(blank=True,max_length=250,verbose_name=_(u'main URI'),editable=True) 92 | created = exfields.CreationDateTimeField(_(u'created'),null=True) 93 | modified = exfields.ModificationDateTimeField(_(u'modified'),null=True) 94 | definition = models.TextField(_(u'definition'), blank=True) 95 | meta = models.TextField(_(u'additional metadata'), help_text=_(u'( ; list) '), blank=True) 96 | def __unicode__(self): 97 | return self.pref_label 98 | 99 | 100 | def natural_key(self): 101 | return( self.uri ) 102 | 103 | def get_absolute_url(self): 104 | return reverse('scheme_detail', args=[self.slug]) 105 | def tree(self): 106 | tree = (self, []) # result is a tuple(scheme,[child concepts]) 107 | for concept in Concept.objects.filter(scheme=self,top_concept=True): 108 | tree[1].append((concept,concept.get_narrower_concepts())) 109 | #with nested tuple(concept, [child concepts]) 110 | return tree 111 | def test_tree(self): 112 | i = self.tree() 113 | print i[0] 114 | for j in i[1]: 115 | print u'--' +unicode(j[0]) 116 | for k in j[1]: 117 | print u'----' + unicode(k[0]) 118 | for l in k[1]: 119 | print u'------' + unicode(l[0]) 120 | for m in l[1]: 121 | print u'--------' + unicode(m[0]) 122 | 123 | for idx, val in enumerate(ints): 124 | print idx, val 125 | 126 | # needs fixing - has limited depth of traversal - probably want an option to paginate and limit. 127 | def json_tree(self,admin_url=False): 128 | i = self.tree() 129 | prefix = '/admin' if admin_url else '' 130 | ja_tree = {'name' : i[0].pref_label, 'children' : []} 131 | for jdx, j in enumerate(i[1]):#j[0] is a concept, j[1] a list of child concepts 132 | ja_tree['children'].append({'name':j[0].pref_label, 133 | 'url':prefix+'/skosxl/concept/'+str(j[0].id)+'/', 134 | 'children':[]}) 135 | for kdx, k in enumerate(j[1]): 136 | ja_tree['children'][jdx]['children'].append({'name':k[0].pref_label, 137 | 'url':prefix+'/skosxl/concept/'+str(k[0].id)+'/', 138 | 'children':[]}) 139 | for ldx,l in enumerate(k[1]): 140 | ja_tree['children'][jdx]['children'][kdx]['children'].append({'name':l[0].pref_label, 141 | 'url':prefix+'/skosxl/concept/'+str(l[0].id)+'/', 142 | 'children':[]}) 143 | for mdx, m in enumerate(l[1]): 144 | ja_tree['children'][jdx]['children'][kdx]['children'][ldx]['children'].append({'name':m[0].pref_label, 145 | 'url':prefix+'/skosxl/concept/'+str(m[0].id)+'/', 146 | #'children':[] #stop 147 | }) 148 | return json.dumps(ja_tree) 149 | 150 | class SchemeMeta(models.Model): 151 | """ 152 | extensible metadata using rdf_io managed reusable generic metadata properties 153 | """ 154 | scheme = models.ForeignKey(Scheme) 155 | metaprop = models.ForeignKey(GenericMetaProp) 156 | value = models.CharField(_(u'value'),max_length=500) 157 | 158 | class ConceptManager(models.Manager): 159 | def get_by_natural_key(self, scheme, term): 160 | return self.get( scheme__uri = scheme, term=term) 161 | 162 | class Concept(models.Model): 163 | objects = ConceptManager() 164 | # this will be the 165 | term = models.CharField(_(u'term'),blank=True,null=True,max_length=255) 166 | # not sure we will need this - SKOS names should enforce slug compatibility. 167 | slug = exfields.AutoSlugField(populate_from=('term')) 168 | pref_label = models.CharField(_(u'preferred label'),blank=True,null=True,max_length=255) 169 | 170 | 171 | definition = models.TextField(_(u'definition'), blank=True) 172 | # notation = models.CharField(blank=True, null=True, max_length=100) 173 | scheme = models.ForeignKey(Scheme, blank=True, null=True) 174 | changenote = models.TextField(_(u'change note'),blank=True) 175 | created = exfields.CreationDateTimeField(_(u'created')) 176 | modified = exfields.ModificationDateTimeField(_(u'modified')) 177 | status = models.PositiveSmallIntegerField( _(u'review status'), 178 | choices=REVIEW_STATUS.CHOICES, 179 | default=REVIEW_STATUS.active) 180 | user = models.ForeignKey(settings.AUTH_USER_MODEL,blank=True,null=True,verbose_name=_(u'django user'),editable=False) 181 | uri = models.CharField(blank=True,max_length=250,verbose_name=_(u'main URI'),editable=False) 182 | author_uri = models.CharField(blank=True,max_length=250,verbose_name=_(u'main URI'),editable=False) 183 | 184 | top_concept = models.BooleanField(default=False, verbose_name=_(u'is top concept')) 185 | sem_relatons = models.ManyToManyField( "self",symmetrical=False, 186 | through='SemRelation', 187 | verbose_name=(_(u'semantic relations'))) 188 | def __unicode__(self): 189 | return "/".join(self.natural_key()) 190 | 191 | def natural_key(self): 192 | return ( self.scheme.natural_key(), self.term, ) 193 | natural_key.dependencies = ['scheme'] 194 | 195 | def get_absolute_url(self): 196 | return reverse('concept_detail', args=[self.id]) 197 | def save(self,skip_name_lookup=False, *args, **kwargs): 198 | if self.scheme is None: 199 | self.scheme = Scheme.objects.get(slug=DEFAULT_SCHEME_SLUG) 200 | if not skip_name_lookup: #updating the pref_label 201 | try: 202 | lookup_label = self.labels.get(language=DEFAULT_LANG,label_type=LABEL_TYPES.prefLabel) 203 | label = lookup_label.label_text 204 | except Label.DoesNotExist: 205 | label = '< no label >' 206 | self.pref_label = label 207 | #self.save(skip_name_lookup=True) 208 | super(Concept, self).save(*args, **kwargs) 209 | def get_narrower_concepts(self): 210 | childs = [] 211 | if SemRelation.objects.filter(origin_concept=self,rel_type=1).exists(): 212 | for narrower in SemRelation.objects.filter(origin_concept=self,rel_type=1): 213 | childs.append(( narrower.target_concept, 214 | narrower.target_concept.get_narrower_concepts() 215 | )) 216 | return childs 217 | 218 | def get_related_term(self, ns) : 219 | """ 220 | dumb - just finds first related term - assumes a 1:1 skos:closeMatch semantics 221 | """ 222 | mr = MapRelation.objects.filter(origin_concept = self, uri__startswith = ns ) 223 | if mr : 224 | return(mr[0].uri[mr[0].uri.rfind('/')+1:]) 225 | return None 226 | 227 | class Meta : 228 | unique_together = (('scheme', 'term'),) 229 | 230 | class Notation(models.Model): 231 | concept = models.ForeignKey(Concept,blank=True,null=True,verbose_name=_(u'main concept'),related_name='notations') 232 | code = models.CharField(_(u'notation'),max_length=10, null=False) 233 | namespace = models.ForeignKey(Namespace,verbose_name=_(u'namespace(type)')) 234 | def __unicode__(self): 235 | return self.code + '^^<' + self.namespace.uri + '>' 236 | class Meta: 237 | verbose_name = _(u'SKOS notation') 238 | verbose_name_plural = _(u'notations') 239 | 240 | 241 | class Label(models.Model): 242 | ''' 243 | Defines a SKOS-XL Label Class, and also a Tag in django-taggit 244 | ''' 245 | # FIELDS name and slug are defined in TagBase - they are forced to be unique 246 | # so if a concept is to be made available as a tag then it must conform to this constraint - generating a label without a Concept implies its is a tag generation - and name will be forced to be unique. 247 | concept = models.ForeignKey(Concept,blank=True,null=True,verbose_name=_(u'main concept'),related_name='labels') 248 | label_type = models.PositiveSmallIntegerField(_(u'label type'), choices=tuple(LABEL_TYPES.CHOICES), default= LABEL_TYPES.prefLabel) 249 | label_text = models.CharField(_(u'label text'),max_length=100, null=False) 250 | language = models.CharField(_(u'language'),max_length=10, choices=LANG_LABELS, default='fr') 251 | 252 | #metadata 253 | user = models.ForeignKey(settings.AUTH_USER_MODEL,blank=True,null=True,verbose_name=_(u'django user'),editable=False) 254 | uri = models.CharField(_(u'author URI'),blank=True,max_length=250,editable=True) 255 | author_uri = models.CharField(u'main URI',blank=True,max_length=250,editable=True) 256 | created = exfields.CreationDateTimeField(_(u'created')) 257 | modified = exfields.ModificationDateTimeField(_(u'modified')) 258 | 259 | 260 | def get_absolute_url(self): 261 | return reverse('tag_detail', args=[self.slug]) 262 | def __unicode__(self): 263 | return unicode(self.label_text) 264 | def create_concept_from_label(self): 265 | if not self.concept: 266 | # self.label_text = self.name 267 | c = Concept(pref_label=self.__unicode__(), 268 | changenote=unicode(ugettext_lazy(u'Created from tag "')+self.__unicode__()+u'"')) 269 | c.save(skip_name_lookup=True)# because we just set it 270 | self.concept = c 271 | self.save() 272 | 273 | def save(self, *args, **kwargs): 274 | # if not self.name : 275 | # self.name = self.label_text 276 | if self.label_type == LABEL_TYPES.prefLabel: 277 | if Label.objects.filter(concept=self.concept, 278 | label_type=LABEL_TYPES.prefLabel, 279 | language=self.language 280 | ).exists(): 281 | raise ValidationError(_(u'There can be only one preferred label by language')) 282 | if not self.concept.pref_label or self.concept.pref_label == '' : 283 | self.concept.pref_label = self.label_text 284 | self.concept.save() 285 | super(Label, self).save() 286 | 287 | #class LabelledItem(GenericTaggedItemBase): 288 | # tag = models.ForeignKey(Label, related_name="skosxl_label_items") 289 | 290 | 291 | def create_reverse_relation(concept,rel_type): 292 | print 'creating inverse relation' 293 | new_rel = SemRelation( origin_concept=concept.target_concept, 294 | target_concept=concept.origin_concept, 295 | rel_type=rel_type) 296 | new_rel.save(skip_inf=True) 297 | 298 | 299 | class SemRelation(models.Model): 300 | ''' 301 | A model linking two skos:Concept 302 | Defines a sub-property of skos:semanticRelation property from the origin concept to the target concept 303 | ''' 304 | origin_concept = models.ForeignKey(Concept,related_name='rel_origin',verbose_name=(_(u'Origin'))) 305 | target_concept = models.ForeignKey(Concept,related_name='rel_target',verbose_name=(_(u'Target'))) 306 | rel_type = models.PositiveSmallIntegerField( _(u'Type of semantic relation'),choices=REL_TYPES.CHOICES, 307 | default=REL_TYPES.narrower) 308 | 309 | # rel_type = models.ForeignKey(RelationType, related_name='curl', verbose_name=_(u'Type of semantic relation')) 310 | class Meta: 311 | verbose_name = _(u'Semantic relation') 312 | verbose_name_plural = _(u'Semantic relations') 313 | 314 | def save(self,skip_inf=False, *args, **kwargs): 315 | if not skip_inf: 316 | if self.rel_type in reverse_map : 317 | create_reverse_relation(self,reverse_map[self.rel_type]) 318 | 319 | super(SemRelation, self).save(*args, **kwargs) 320 | 321 | 322 | # 323 | # class Vocabulary(models.Model): 324 | # ''' 325 | # A remote SKOS Thesaurus 326 | # ''' 327 | # name = models.CharField(_(u'Name'),max_length=100) 328 | # info_url = models.URLField(_(u'URL'),blank=True, verify_exists=False) 329 | # class Meta: 330 | # verbose_name = _(u'SKOS Thesaurus') 331 | # verbose_name_plural = _(u'SKOS Thesaurii') 332 | # def __unicode__(self): 333 | # return self.name 334 | # 335 | class MapRelation(models.Model): 336 | 337 | origin_concept = models.ForeignKey(Concept,related_name='map_origin',verbose_name=(_(u'Local concept to map'))) 338 | # target_concept = models.ForeignKey(Concept,related_name='map_target',verbose_name=(_(u'Remote concept')),blank=True, null=True) 339 | # target_label = models.CharField(_(u'Preferred label'),max_length=255)#nan nan il faut un autre concept stocké dans un scheme 340 | uri = models.CharField(_(u'Target Concept URI'), max_length=250) 341 | # voc = models.ForeignKey(Vocabulary, verbose_name=(_(u'SKOS Thesaurus'))) 342 | match_type = models.PositiveSmallIntegerField( _(u'Type of mapping relation'), 343 | choices=MATCH_TYPES.CHOICES, 344 | default=MATCH_TYPES.closeMatch) 345 | class Meta: 346 | verbose_name = _(u'Mapping relation') 347 | verbose_name_plural = _(u'Mapping relations') 348 | # 349 | 350 | -------------------------------------------------------------------------------- /skosxl/static/d3.layout.js: -------------------------------------------------------------------------------- 1 | (function(){d3.layout = {}; 2 | // Implements hierarchical edge bundling using Holten's algorithm. For each 3 | // input link, a path is computed that travels through the tree, up the parent 4 | // hierarchy to the least common ancestor, and then back down to the destination 5 | // node. Each path is simply an array of nodes. 6 | d3.layout.bundle = function() { 7 | return function(links) { 8 | var paths = [], 9 | i = -1, 10 | n = links.length; 11 | while (++i < n) paths.push(d3_layout_bundlePath(links[i])); 12 | return paths; 13 | }; 14 | }; 15 | 16 | function d3_layout_bundlePath(link) { 17 | var start = link.source, 18 | end = link.target, 19 | lca = d3_layout_bundleLeastCommonAncestor(start, end), 20 | points = [start]; 21 | while (start !== lca) { 22 | start = start.parent; 23 | points.push(start); 24 | } 25 | var k = points.length; 26 | while (end !== lca) { 27 | points.splice(k, 0, end); 28 | end = end.parent; 29 | } 30 | return points; 31 | } 32 | 33 | function d3_layout_bundleAncestors(node) { 34 | var ancestors = [], 35 | parent = node.parent; 36 | while (parent != null) { 37 | ancestors.push(node); 38 | node = parent; 39 | parent = parent.parent; 40 | } 41 | ancestors.push(node); 42 | return ancestors; 43 | } 44 | 45 | function d3_layout_bundleLeastCommonAncestor(a, b) { 46 | if (a === b) return a; 47 | var aNodes = d3_layout_bundleAncestors(a), 48 | bNodes = d3_layout_bundleAncestors(b), 49 | aNode = aNodes.pop(), 50 | bNode = bNodes.pop(), 51 | sharedNode = null; 52 | while (aNode === bNode) { 53 | sharedNode = aNode; 54 | aNode = aNodes.pop(); 55 | bNode = bNodes.pop(); 56 | } 57 | return sharedNode; 58 | } 59 | d3.layout.chord = function() { 60 | var chord = {}, 61 | chords, 62 | groups, 63 | matrix, 64 | n, 65 | padding = 0, 66 | sortGroups, 67 | sortSubgroups, 68 | sortChords; 69 | 70 | function relayout() { 71 | var subgroups = {}, 72 | groupSums = [], 73 | groupIndex = d3.range(n), 74 | subgroupIndex = [], 75 | k, 76 | x, 77 | x0, 78 | i, 79 | j; 80 | 81 | chords = []; 82 | groups = []; 83 | 84 | // Compute the sum. 85 | k = 0, i = -1; while (++i < n) { 86 | x = 0, j = -1; while (++j < n) { 87 | x += matrix[i][j]; 88 | } 89 | groupSums.push(x); 90 | subgroupIndex.push(d3.range(n)); 91 | k += x; 92 | } 93 | 94 | // Sort groups… 95 | if (sortGroups) { 96 | groupIndex.sort(function(a, b) { 97 | return sortGroups(groupSums[a], groupSums[b]); 98 | }); 99 | } 100 | 101 | // Sort subgroups… 102 | if (sortSubgroups) { 103 | subgroupIndex.forEach(function(d, i) { 104 | d.sort(function(a, b) { 105 | return sortSubgroups(matrix[i][a], matrix[i][b]); 106 | }); 107 | }); 108 | } 109 | 110 | // Convert the sum to scaling factor for [0, 2pi]. 111 | // TODO Allow start and end angle to be specified. 112 | // TODO Allow padding to be specified as percentage? 113 | k = (2 * Math.PI - padding * n) / k; 114 | 115 | // Compute the start and end angle for each group and subgroup. 116 | // Note: Opera has a bug reordering object literal properties! 117 | x = 0, i = -1; while (++i < n) { 118 | x0 = x, j = -1; while (++j < n) { 119 | var di = groupIndex[i], 120 | dj = subgroupIndex[di][j], 121 | v = matrix[di][dj], 122 | a0 = x, 123 | a1 = x += v * k; 124 | subgroups[di + "-" + dj] = { 125 | index: di, 126 | subindex: dj, 127 | startAngle: a0, 128 | endAngle: a1, 129 | value: v 130 | }; 131 | } 132 | groups.push({ 133 | index: di, 134 | startAngle: x0, 135 | endAngle: x, 136 | value: (x - x0) / k 137 | }); 138 | x += padding; 139 | } 140 | 141 | // Generate chords for each (non-empty) subgroup-subgroup link. 142 | i = -1; while (++i < n) { 143 | j = i - 1; while (++j < n) { 144 | var source = subgroups[i + "-" + j], 145 | target = subgroups[j + "-" + i]; 146 | if (source.value || target.value) { 147 | chords.push(source.value < target.value 148 | ? {source: target, target: source} 149 | : {source: source, target: target}); 150 | } 151 | } 152 | } 153 | 154 | if (sortChords) resort(); 155 | } 156 | 157 | function resort() { 158 | chords.sort(function(a, b) { 159 | return sortChords( 160 | (a.source.value + a.target.value) / 2, 161 | (b.source.value + b.target.value) / 2); 162 | }); 163 | } 164 | 165 | chord.matrix = function(x) { 166 | if (!arguments.length) return matrix; 167 | n = (matrix = x) && matrix.length; 168 | chords = groups = null; 169 | return chord; 170 | }; 171 | 172 | chord.padding = function(x) { 173 | if (!arguments.length) return padding; 174 | padding = x; 175 | chords = groups = null; 176 | return chord; 177 | }; 178 | 179 | chord.sortGroups = function(x) { 180 | if (!arguments.length) return sortGroups; 181 | sortGroups = x; 182 | chords = groups = null; 183 | return chord; 184 | }; 185 | 186 | chord.sortSubgroups = function(x) { 187 | if (!arguments.length) return sortSubgroups; 188 | sortSubgroups = x; 189 | chords = null; 190 | return chord; 191 | }; 192 | 193 | chord.sortChords = function(x) { 194 | if (!arguments.length) return sortChords; 195 | sortChords = x; 196 | if (chords) resort(); 197 | return chord; 198 | }; 199 | 200 | chord.chords = function() { 201 | if (!chords) relayout(); 202 | return chords; 203 | }; 204 | 205 | chord.groups = function() { 206 | if (!groups) relayout(); 207 | return groups; 208 | }; 209 | 210 | return chord; 211 | }; 212 | // A rudimentary force layout using Gauss-Seidel. 213 | d3.layout.force = function() { 214 | var force = {}, 215 | event = d3.dispatch("tick"), 216 | size = [1, 1], 217 | drag, 218 | alpha, 219 | friction = .9, 220 | linkDistance = d3_layout_forceLinkDistance, 221 | linkStrength = d3_layout_forceLinkStrength, 222 | charge = -30, 223 | gravity = .1, 224 | theta = .8, 225 | interval, 226 | nodes = [], 227 | links = [], 228 | distances, 229 | strengths, 230 | charges; 231 | 232 | function repulse(node) { 233 | return function(quad, x1, y1, x2, y2) { 234 | if (quad.point !== node) { 235 | var dx = quad.cx - node.x, 236 | dy = quad.cy - node.y, 237 | dn = 1 / Math.sqrt(dx * dx + dy * dy); 238 | 239 | /* Barnes-Hut criterion. */ 240 | if ((x2 - x1) * dn < theta) { 241 | var k = quad.charge * dn * dn; 242 | node.px -= dx * k; 243 | node.py -= dy * k; 244 | return true; 245 | } 246 | 247 | if (quad.point && isFinite(dn)) { 248 | var k = quad.pointCharge * dn * dn; 249 | node.px -= dx * k; 250 | node.py -= dy * k; 251 | } 252 | } 253 | return !quad.charge; 254 | }; 255 | } 256 | 257 | function tick() { 258 | var n = nodes.length, 259 | m = links.length, 260 | q, 261 | i, // current index 262 | o, // current object 263 | s, // current source 264 | t, // current target 265 | l, // current distance 266 | k, // current force 267 | x, // x-distance 268 | y; // y-distance 269 | 270 | // gauss-seidel relaxation for links 271 | for (i = 0; i < m; ++i) { 272 | o = links[i]; 273 | s = o.source; 274 | t = o.target; 275 | x = t.x - s.x; 276 | y = t.y - s.y; 277 | if (l = (x * x + y * y)) { 278 | l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; 279 | x *= l; 280 | y *= l; 281 | t.x -= x * (k = s.weight / (t.weight + s.weight)); 282 | t.y -= y * k; 283 | s.x += x * (k = 1 - k); 284 | s.y += y * k; 285 | } 286 | } 287 | 288 | // apply gravity forces 289 | if (k = alpha * gravity) { 290 | x = size[0] / 2; 291 | y = size[1] / 2; 292 | i = -1; if (k) while (++i < n) { 293 | o = nodes[i]; 294 | o.x += (x - o.x) * k; 295 | o.y += (y - o.y) * k; 296 | } 297 | } 298 | 299 | // compute quadtree center of mass and apply charge forces 300 | if (charge) { 301 | d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); 302 | i = -1; while (++i < n) { 303 | if (!(o = nodes[i]).fixed) { 304 | q.visit(repulse(o)); 305 | } 306 | } 307 | } 308 | 309 | // position verlet integration 310 | i = -1; while (++i < n) { 311 | o = nodes[i]; 312 | if (o.fixed) { 313 | o.x = o.px; 314 | o.y = o.py; 315 | } else { 316 | o.x -= (o.px - (o.px = o.x)) * friction; 317 | o.y -= (o.py - (o.py = o.y)) * friction; 318 | } 319 | } 320 | 321 | event.tick({type: "tick", alpha: alpha}); 322 | 323 | // simulated annealing, basically 324 | return (alpha *= .99) < .005; 325 | } 326 | 327 | force.nodes = function(x) { 328 | if (!arguments.length) return nodes; 329 | nodes = x; 330 | return force; 331 | }; 332 | 333 | force.links = function(x) { 334 | if (!arguments.length) return links; 335 | links = x; 336 | return force; 337 | }; 338 | 339 | force.size = function(x) { 340 | if (!arguments.length) return size; 341 | size = x; 342 | return force; 343 | }; 344 | 345 | force.linkDistance = function(x) { 346 | if (!arguments.length) return linkDistance; 347 | linkDistance = d3.functor(x); 348 | return force; 349 | }; 350 | 351 | // For backwards-compatibility. 352 | force.distance = force.linkDistance; 353 | 354 | force.linkStrength = function(x) { 355 | if (!arguments.length) return linkStrength; 356 | linkStrength = d3.functor(x); 357 | return force; 358 | }; 359 | 360 | force.friction = function(x) { 361 | if (!arguments.length) return friction; 362 | friction = x; 363 | return force; 364 | }; 365 | 366 | force.charge = function(x) { 367 | if (!arguments.length) return charge; 368 | charge = typeof x === "function" ? x : +x; 369 | return force; 370 | }; 371 | 372 | force.gravity = function(x) { 373 | if (!arguments.length) return gravity; 374 | gravity = x; 375 | return force; 376 | }; 377 | 378 | force.theta = function(x) { 379 | if (!arguments.length) return theta; 380 | theta = x; 381 | return force; 382 | }; 383 | 384 | force.start = function() { 385 | var i, 386 | j, 387 | n = nodes.length, 388 | m = links.length, 389 | w = size[0], 390 | h = size[1], 391 | neighbors, 392 | o; 393 | 394 | for (i = 0; i < n; ++i) { 395 | (o = nodes[i]).index = i; 396 | o.weight = 0; 397 | } 398 | 399 | distances = []; 400 | strengths = []; 401 | for (i = 0; i < m; ++i) { 402 | o = links[i]; 403 | if (typeof o.source == "number") o.source = nodes[o.source]; 404 | if (typeof o.target == "number") o.target = nodes[o.target]; 405 | distances[i] = linkDistance.call(this, o, i); 406 | strengths[i] = linkStrength.call(this, o, i); 407 | ++o.source.weight; 408 | ++o.target.weight; 409 | } 410 | 411 | for (i = 0; i < n; ++i) { 412 | o = nodes[i]; 413 | if (isNaN(o.x)) o.x = position("x", w); 414 | if (isNaN(o.y)) o.y = position("y", h); 415 | if (isNaN(o.px)) o.px = o.x; 416 | if (isNaN(o.py)) o.py = o.y; 417 | } 418 | 419 | charges = []; 420 | if (typeof charge === "function") { 421 | for (i = 0; i < n; ++i) { 422 | charges[i] = +charge.call(this, nodes[i], i); 423 | } 424 | } else { 425 | for (i = 0; i < n; ++i) { 426 | charges[i] = charge; 427 | } 428 | } 429 | 430 | // initialize node position based on first neighbor 431 | function position(dimension, size) { 432 | var neighbors = neighbor(i), 433 | j = -1, 434 | m = neighbors.length, 435 | x; 436 | while (++j < m) if (!isNaN(x = neighbors[j][dimension])) return x; 437 | return Math.random() * size; 438 | } 439 | 440 | // initialize neighbors lazily 441 | function neighbor() { 442 | if (!neighbors) { 443 | neighbors = []; 444 | for (j = 0; j < n; ++j) { 445 | neighbors[j] = []; 446 | } 447 | for (j = 0; j < m; ++j) { 448 | var o = links[j]; 449 | neighbors[o.source.index].push(o.target); 450 | neighbors[o.target.index].push(o.source); 451 | } 452 | } 453 | return neighbors[i]; 454 | } 455 | 456 | return force.resume(); 457 | }; 458 | 459 | force.resume = function() { 460 | alpha = .1; 461 | d3.timer(tick); 462 | return force; 463 | }; 464 | 465 | force.stop = function() { 466 | alpha = 0; 467 | return force; 468 | }; 469 | 470 | // use `node.call(force.drag)` to make nodes draggable 471 | force.drag = function() { 472 | if (!drag) drag = d3.behavior.drag() 473 | .origin(Object) 474 | .on("dragstart", dragstart) 475 | .on("drag", d3_layout_forceDrag) 476 | .on("dragend", d3_layout_forceDragEnd); 477 | 478 | this.on("mouseover.force", d3_layout_forceDragOver) 479 | .on("mouseout.force", d3_layout_forceDragOut) 480 | .call(drag); 481 | }; 482 | 483 | function dragstart(d) { 484 | d3_layout_forceDragOver(d3_layout_forceDragNode = d); 485 | d3_layout_forceDragForce = force; 486 | } 487 | 488 | return d3.rebind(force, event, "on"); 489 | }; 490 | 491 | var d3_layout_forceDragForce, 492 | d3_layout_forceDragNode; 493 | 494 | function d3_layout_forceDragOver(d) { 495 | d.fixed |= 2; 496 | } 497 | 498 | function d3_layout_forceDragOut(d) { 499 | if (d !== d3_layout_forceDragNode) d.fixed &= 1; 500 | } 501 | 502 | function d3_layout_forceDragEnd() { 503 | d3_layout_forceDrag(); 504 | d3_layout_forceDragNode.fixed &= 1; 505 | d3_layout_forceDragForce = d3_layout_forceDragNode = null; 506 | } 507 | 508 | function d3_layout_forceDrag() { 509 | d3_layout_forceDragNode.px = d3.event.x; 510 | d3_layout_forceDragNode.py = d3.event.y; 511 | d3_layout_forceDragForce.resume(); // restart annealing 512 | } 513 | 514 | function d3_layout_forceAccumulate(quad, alpha, charges) { 515 | var cx = 0, 516 | cy = 0; 517 | quad.charge = 0; 518 | if (!quad.leaf) { 519 | var nodes = quad.nodes, 520 | n = nodes.length, 521 | i = -1, 522 | c; 523 | while (++i < n) { 524 | c = nodes[i]; 525 | if (c == null) continue; 526 | d3_layout_forceAccumulate(c, alpha, charges); 527 | quad.charge += c.charge; 528 | cx += c.charge * c.cx; 529 | cy += c.charge * c.cy; 530 | } 531 | } 532 | if (quad.point) { 533 | // jitter internal nodes that are coincident 534 | if (!quad.leaf) { 535 | quad.point.x += Math.random() - .5; 536 | quad.point.y += Math.random() - .5; 537 | } 538 | var k = alpha * charges[quad.point.index]; 539 | quad.charge += quad.pointCharge = k; 540 | cx += k * quad.point.x; 541 | cy += k * quad.point.y; 542 | } 543 | quad.cx = cx / quad.charge; 544 | quad.cy = cy / quad.charge; 545 | } 546 | 547 | function d3_layout_forceLinkDistance(link) { 548 | return 20; 549 | } 550 | 551 | function d3_layout_forceLinkStrength(link) { 552 | return 1; 553 | } 554 | d3.layout.partition = function() { 555 | var hierarchy = d3.layout.hierarchy(), 556 | size = [1, 1]; // width, height 557 | 558 | function position(node, x, dx, dy) { 559 | var children = node.children; 560 | node.x = x; 561 | node.y = node.depth * dy; 562 | node.dx = dx; 563 | node.dy = dy; 564 | if (children && (n = children.length)) { 565 | var i = -1, 566 | n, 567 | c, 568 | d; 569 | dx = node.value ? dx / node.value : 0; 570 | while (++i < n) { 571 | position(c = children[i], x, d = c.value * dx, dy); 572 | x += d; 573 | } 574 | } 575 | } 576 | 577 | function depth(node) { 578 | var children = node.children, 579 | d = 0; 580 | if (children && (n = children.length)) { 581 | var i = -1, 582 | n; 583 | while (++i < n) d = Math.max(d, depth(children[i])); 584 | } 585 | return 1 + d; 586 | } 587 | 588 | function partition(d, i) { 589 | var nodes = hierarchy.call(this, d, i); 590 | position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); 591 | return nodes; 592 | } 593 | 594 | partition.size = function(x) { 595 | if (!arguments.length) return size; 596 | size = x; 597 | return partition; 598 | }; 599 | 600 | return d3_layout_hierarchyRebind(partition, hierarchy); 601 | }; 602 | d3.layout.pie = function() { 603 | var value = Number, 604 | sort = d3_layout_pieSortByValue, 605 | startAngle = 0, 606 | endAngle = 2 * Math.PI; 607 | 608 | function pie(data, i) { 609 | 610 | // Compute the numeric values for each data element. 611 | var values = data.map(function(d, i) { return +value.call(pie, d, i); }); 612 | 613 | // Compute the start angle. 614 | var a = +(typeof startAngle === "function" 615 | ? startAngle.apply(this, arguments) 616 | : startAngle); 617 | 618 | // Compute the angular scale factor: from value to radians. 619 | var k = ((typeof endAngle === "function" 620 | ? endAngle.apply(this, arguments) 621 | : endAngle) - startAngle) 622 | / d3.sum(values); 623 | 624 | // Optionally sort the data. 625 | var index = d3.range(data.length); 626 | if (sort != null) index.sort(sort === d3_layout_pieSortByValue 627 | ? function(i, j) { return values[j] - values[i]; } 628 | : function(i, j) { return sort(data[i], data[j]); }); 629 | 630 | // Compute the arcs! 631 | // They are stored in the original data's order. 632 | var arcs = []; 633 | index.forEach(function(i) { 634 | arcs[i] = { 635 | data: data[i], 636 | value: d = values[i], 637 | startAngle: a, 638 | endAngle: a += d * k 639 | }; 640 | }); 641 | return arcs; 642 | } 643 | 644 | /** 645 | * Specifies the value function *x*, which returns a nonnegative numeric value 646 | * for each datum. The default value function is `Number`. The value function 647 | * is passed two arguments: the current datum and the current index. 648 | */ 649 | pie.value = function(x) { 650 | if (!arguments.length) return value; 651 | value = x; 652 | return pie; 653 | }; 654 | 655 | /** 656 | * Specifies a sort comparison operator *x*. The comparator is passed two data 657 | * elements from the data array, a and b; it returns a negative value if a is 658 | * less than b, a positive value if a is greater than b, and zero if a equals 659 | * b. 660 | */ 661 | pie.sort = function(x) { 662 | if (!arguments.length) return sort; 663 | sort = x; 664 | return pie; 665 | }; 666 | 667 | /** 668 | * Specifies the overall start angle of the pie chart. Defaults to 0. The 669 | * start angle can be specified either as a constant or as a function; in the 670 | * case of a function, it is evaluated once per array (as opposed to per 671 | * element). 672 | */ 673 | pie.startAngle = function(x) { 674 | if (!arguments.length) return startAngle; 675 | startAngle = x; 676 | return pie; 677 | }; 678 | 679 | /** 680 | * Specifies the overall end angle of the pie chart. Defaults to 2Ï€. The 681 | * end angle can be specified either as a constant or as a function; in the 682 | * case of a function, it is evaluated once per array (as opposed to per 683 | * element). 684 | */ 685 | pie.endAngle = function(x) { 686 | if (!arguments.length) return endAngle; 687 | endAngle = x; 688 | return pie; 689 | }; 690 | 691 | return pie; 692 | }; 693 | 694 | var d3_layout_pieSortByValue = {}; 695 | // data is two-dimensional array of x,y; we populate y0 696 | d3.layout.stack = function() { 697 | var values = Object, 698 | order = d3_layout_stackOrders["default"], 699 | offset = d3_layout_stackOffsets["zero"], 700 | out = d3_layout_stackOut, 701 | x = d3_layout_stackX, 702 | y = d3_layout_stackY; 703 | 704 | function stack(data, index) { 705 | 706 | // Convert series to canonical two-dimensional representation. 707 | var series = data.map(function(d, i) { 708 | return values.call(stack, d, i); 709 | }); 710 | 711 | // Convert each series to canonical [[x,y]] representation. 712 | var points = series.map(function(d, i) { 713 | return d.map(function(v, i) { 714 | return [x.call(stack, v, i), y.call(stack, v, i)]; 715 | }); 716 | }); 717 | 718 | // Compute the order of series, and permute them. 719 | var orders = order.call(stack, points, index); 720 | series = d3.permute(series, orders); 721 | points = d3.permute(points, orders); 722 | 723 | // Compute the baseline… 724 | var offsets = offset.call(stack, points, index); 725 | 726 | // And propagate it to other series. 727 | var n = series.length, 728 | m = series[0].length, 729 | i, 730 | j, 731 | o; 732 | for (j = 0; j < m; ++j) { 733 | out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); 734 | for (i = 1; i < n; ++i) { 735 | out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); 736 | } 737 | } 738 | 739 | return data; 740 | } 741 | 742 | stack.values = function(x) { 743 | if (!arguments.length) return values; 744 | values = x; 745 | return stack; 746 | }; 747 | 748 | stack.order = function(x) { 749 | if (!arguments.length) return order; 750 | order = typeof x === "function" ? x : d3_layout_stackOrders[x]; 751 | return stack; 752 | }; 753 | 754 | stack.offset = function(x) { 755 | if (!arguments.length) return offset; 756 | offset = typeof x === "function" ? x : d3_layout_stackOffsets[x]; 757 | return stack; 758 | }; 759 | 760 | stack.x = function(z) { 761 | if (!arguments.length) return x; 762 | x = z; 763 | return stack; 764 | }; 765 | 766 | stack.y = function(z) { 767 | if (!arguments.length) return y; 768 | y = z; 769 | return stack; 770 | }; 771 | 772 | stack.out = function(z) { 773 | if (!arguments.length) return out; 774 | out = z; 775 | return stack; 776 | }; 777 | 778 | return stack; 779 | } 780 | 781 | function d3_layout_stackX(d) { 782 | return d.x; 783 | } 784 | 785 | function d3_layout_stackY(d) { 786 | return d.y; 787 | } 788 | 789 | function d3_layout_stackOut(d, y0, y) { 790 | d.y0 = y0; 791 | d.y = y; 792 | } 793 | 794 | var d3_layout_stackOrders = { 795 | 796 | "inside-out": function(data) { 797 | var n = data.length, 798 | i, 799 | j, 800 | max = data.map(d3_layout_stackMaxIndex), 801 | sums = data.map(d3_layout_stackReduceSum), 802 | index = d3.range(n).sort(function(a, b) { return max[a] - max[b]; }), 803 | top = 0, 804 | bottom = 0, 805 | tops = [], 806 | bottoms = []; 807 | for (i = 0; i < n; ++i) { 808 | j = index[i]; 809 | if (top < bottom) { 810 | top += sums[j]; 811 | tops.push(j); 812 | } else { 813 | bottom += sums[j]; 814 | bottoms.push(j); 815 | } 816 | } 817 | return bottoms.reverse().concat(tops); 818 | }, 819 | 820 | "reverse": function(data) { 821 | return d3.range(data.length).reverse(); 822 | }, 823 | 824 | "default": function(data) { 825 | return d3.range(data.length); 826 | } 827 | 828 | }; 829 | 830 | var d3_layout_stackOffsets = { 831 | 832 | "silhouette": function(data) { 833 | var n = data.length, 834 | m = data[0].length, 835 | sums = [], 836 | max = 0, 837 | i, 838 | j, 839 | o, 840 | y0 = []; 841 | for (j = 0; j < m; ++j) { 842 | for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; 843 | if (o > max) max = o; 844 | sums.push(o); 845 | } 846 | for (j = 0; j < m; ++j) { 847 | y0[j] = (max - sums[j]) / 2; 848 | } 849 | return y0; 850 | }, 851 | 852 | "wiggle": function(data) { 853 | var n = data.length, 854 | x = data[0], 855 | m = x.length, 856 | max = 0, 857 | i, 858 | j, 859 | k, 860 | s1, 861 | s2, 862 | s3, 863 | dx, 864 | o, 865 | o0, 866 | y0 = []; 867 | y0[0] = o = o0 = 0; 868 | for (j = 1; j < m; ++j) { 869 | for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; 870 | for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { 871 | for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { 872 | s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; 873 | } 874 | s2 += s3 * data[i][j][1]; 875 | } 876 | y0[j] = o -= s1 ? s2 / s1 * dx : 0; 877 | if (o < o0) o0 = o; 878 | } 879 | for (j = 0; j < m; ++j) y0[j] -= o0; 880 | return y0; 881 | }, 882 | 883 | "expand": function(data) { 884 | var n = data.length, 885 | m = data[0].length, 886 | k = 1 / n, 887 | i, 888 | j, 889 | o, 890 | y0 = []; 891 | for (j = 0; j < m; ++j) { 892 | for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; 893 | if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; 894 | else for (i = 0; i < n; i++) data[i][j][1] = k; 895 | } 896 | for (j = 0; j < m; ++j) y0[j] = 0; 897 | return y0; 898 | }, 899 | 900 | "zero": function(data) { 901 | var j = -1, 902 | m = data[0].length, 903 | y0 = []; 904 | while (++j < m) y0[j] = 0; 905 | return y0; 906 | } 907 | 908 | }; 909 | 910 | function d3_layout_stackMaxIndex(array) { 911 | var i = 1, 912 | j = 0, 913 | v = array[0][1], 914 | k, 915 | n = array.length; 916 | for (; i < n; ++i) { 917 | if ((k = array[i][1]) > v) { 918 | j = i; 919 | v = k; 920 | } 921 | } 922 | return j; 923 | } 924 | 925 | function d3_layout_stackReduceSum(d) { 926 | return d.reduce(d3_layout_stackSum, 0); 927 | } 928 | 929 | function d3_layout_stackSum(p, d) { 930 | return p + d[1]; 931 | } 932 | d3.layout.histogram = function() { 933 | var frequency = true, 934 | valuer = Number, 935 | ranger = d3_layout_histogramRange, 936 | binner = d3_layout_histogramBinSturges; 937 | 938 | function histogram(data, i) { 939 | var bins = [], 940 | values = data.map(valuer, this), 941 | range = ranger.call(this, values, i), 942 | thresholds = binner.call(this, range, values, i), 943 | bin, 944 | i = -1, 945 | n = values.length, 946 | m = thresholds.length - 1, 947 | k = frequency ? 1 : 1 / n, 948 | x; 949 | 950 | // Initialize the bins. 951 | while (++i < m) { 952 | bin = bins[i] = []; 953 | bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); 954 | bin.y = 0; 955 | } 956 | 957 | // Fill the bins, ignoring values outside the range. 958 | i = -1; while(++i < n) { 959 | x = values[i]; 960 | if ((x >= range[0]) && (x <= range[1])) { 961 | bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; 962 | bin.y += k; 963 | bin.push(data[i]); 964 | } 965 | } 966 | 967 | return bins; 968 | } 969 | 970 | // Specifies how to extract a value from the associated data. The default 971 | // value function is `Number`, which is equivalent to the identity function. 972 | histogram.value = function(x) { 973 | if (!arguments.length) return valuer; 974 | valuer = x; 975 | return histogram; 976 | }; 977 | 978 | // Specifies the range of the histogram. Values outside the specified range 979 | // will be ignored. The argument `x` may be specified either as a two-element 980 | // array representing the minimum and maximum value of the range, or as a 981 | // function that returns the range given the array of values and the current 982 | // index `i`. The default range is the extent (minimum and maximum) of the 983 | // values. 984 | histogram.range = function(x) { 985 | if (!arguments.length) return ranger; 986 | ranger = d3.functor(x); 987 | return histogram; 988 | }; 989 | 990 | // Specifies how to bin values in the histogram. The argument `x` may be 991 | // specified as a number, in which case the range of values will be split 992 | // uniformly into the given number of bins. Or, `x` may be an array of 993 | // threshold values, defining the bins; the specified array must contain the 994 | // rightmost (upper) value, thus specifying n + 1 values for n bins. Or, `x` 995 | // may be a function which is evaluated, being passed the range, the array of 996 | // values, and the current index `i`, returning an array of thresholds. The 997 | // default bin function will divide the values into uniform bins using 998 | // Sturges' formula. 999 | histogram.bins = function(x) { 1000 | if (!arguments.length) return binner; 1001 | binner = typeof x === "number" 1002 | ? function(range) { return d3_layout_histogramBinFixed(range, x); } 1003 | : d3.functor(x); 1004 | return histogram; 1005 | }; 1006 | 1007 | // Specifies whether the histogram's `y` value is a count (frequency) or a 1008 | // probability (density). The default value is true. 1009 | histogram.frequency = function(x) { 1010 | if (!arguments.length) return frequency; 1011 | frequency = !!x; 1012 | return histogram; 1013 | }; 1014 | 1015 | return histogram; 1016 | }; 1017 | 1018 | function d3_layout_histogramBinSturges(range, values) { 1019 | return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); 1020 | } 1021 | 1022 | function d3_layout_histogramBinFixed(range, n) { 1023 | var x = -1, 1024 | b = +range[0], 1025 | m = (range[1] - b) / n, 1026 | f = []; 1027 | while (++x <= n) f[x] = m * x + b; 1028 | return f; 1029 | } 1030 | 1031 | function d3_layout_histogramRange(values) { 1032 | return [d3.min(values), d3.max(values)]; 1033 | } 1034 | d3.layout.hierarchy = function() { 1035 | var sort = d3_layout_hierarchySort, 1036 | children = d3_layout_hierarchyChildren, 1037 | value = d3_layout_hierarchyValue; 1038 | 1039 | // Recursively compute the node depth and value. 1040 | // Also converts the data representation into a standard hierarchy structure. 1041 | function recurse(data, depth, nodes) { 1042 | var childs = children.call(hierarchy, data, depth), 1043 | node = d3_layout_hierarchyInline ? data : {data: data}; 1044 | node.depth = depth; 1045 | nodes.push(node); 1046 | if (childs && (n = childs.length)) { 1047 | var i = -1, 1048 | n, 1049 | c = node.children = [], 1050 | v = 0, 1051 | j = depth + 1; 1052 | while (++i < n) { 1053 | d = recurse(childs[i], j, nodes); 1054 | d.parent = node; 1055 | c.push(d); 1056 | v += d.value; 1057 | } 1058 | if (sort) c.sort(sort); 1059 | if (value) node.value = v; 1060 | } else if (value) { 1061 | node.value = +value.call(hierarchy, data, depth) || 0; 1062 | } 1063 | return node; 1064 | } 1065 | 1066 | // Recursively re-evaluates the node value. 1067 | function revalue(node, depth) { 1068 | var children = node.children, 1069 | v = 0; 1070 | if (children && (n = children.length)) { 1071 | var i = -1, 1072 | n, 1073 | j = depth + 1; 1074 | while (++i < n) v += revalue(children[i], j); 1075 | } else if (value) { 1076 | v = +value.call(hierarchy, d3_layout_hierarchyInline ? node : node.data, depth) || 0; 1077 | } 1078 | if (value) node.value = v; 1079 | return v; 1080 | } 1081 | 1082 | function hierarchy(d) { 1083 | var nodes = []; 1084 | recurse(d, 0, nodes); 1085 | return nodes; 1086 | } 1087 | 1088 | hierarchy.sort = function(x) { 1089 | if (!arguments.length) return sort; 1090 | sort = x; 1091 | return hierarchy; 1092 | }; 1093 | 1094 | hierarchy.children = function(x) { 1095 | if (!arguments.length) return children; 1096 | children = x; 1097 | return hierarchy; 1098 | }; 1099 | 1100 | hierarchy.value = function(x) { 1101 | if (!arguments.length) return value; 1102 | value = x; 1103 | return hierarchy; 1104 | }; 1105 | 1106 | // Re-evaluates the `value` property for the specified hierarchy. 1107 | hierarchy.revalue = function(root) { 1108 | revalue(root, 0); 1109 | return root; 1110 | }; 1111 | 1112 | return hierarchy; 1113 | }; 1114 | 1115 | // A method assignment helper for hierarchy subclasses. 1116 | function d3_layout_hierarchyRebind(object, hierarchy) { 1117 | d3.rebind(object, hierarchy, "sort", "children", "value"); 1118 | 1119 | // Add an alias for links, for convenience. 1120 | object.links = d3_layout_hierarchyLinks; 1121 | 1122 | // If the new API is used, enabling inlining. 1123 | object.nodes = function(d) { 1124 | d3_layout_hierarchyInline = true; 1125 | return (object.nodes = object)(d); 1126 | }; 1127 | 1128 | return object; 1129 | } 1130 | 1131 | function d3_layout_hierarchyChildren(d) { 1132 | return d.children; 1133 | } 1134 | 1135 | function d3_layout_hierarchyValue(d) { 1136 | return d.value; 1137 | } 1138 | 1139 | function d3_layout_hierarchySort(a, b) { 1140 | return b.value - a.value; 1141 | } 1142 | 1143 | // Returns an array source+target objects for the specified nodes. 1144 | function d3_layout_hierarchyLinks(nodes) { 1145 | return d3.merge(nodes.map(function(parent) { 1146 | return (parent.children || []).map(function(child) { 1147 | return {source: parent, target: child}; 1148 | }); 1149 | })); 1150 | } 1151 | 1152 | // For backwards-compatibility, don't enable inlining by default. 1153 | var d3_layout_hierarchyInline = false; 1154 | d3.layout.pack = function() { 1155 | var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), 1156 | size = [1, 1]; 1157 | 1158 | function pack(d, i) { 1159 | var nodes = hierarchy.call(this, d, i), 1160 | root = nodes[0]; 1161 | 1162 | // Recursively compute the layout. 1163 | root.x = 0; 1164 | root.y = 0; 1165 | d3_layout_packTree(root); 1166 | 1167 | // Scale the layout to fit the requested size. 1168 | var w = size[0], 1169 | h = size[1], 1170 | k = 1 / Math.max(2 * root.r / w, 2 * root.r / h); 1171 | d3_layout_packTransform(root, w / 2, h / 2, k); 1172 | 1173 | return nodes; 1174 | } 1175 | 1176 | pack.size = function(x) { 1177 | if (!arguments.length) return size; 1178 | size = x; 1179 | return pack; 1180 | }; 1181 | 1182 | return d3_layout_hierarchyRebind(pack, hierarchy); 1183 | }; 1184 | 1185 | function d3_layout_packSort(a, b) { 1186 | return a.value - b.value; 1187 | } 1188 | 1189 | function d3_layout_packInsert(a, b) { 1190 | var c = a._pack_next; 1191 | a._pack_next = b; 1192 | b._pack_prev = a; 1193 | b._pack_next = c; 1194 | c._pack_prev = b; 1195 | } 1196 | 1197 | function d3_layout_packSplice(a, b) { 1198 | a._pack_next = b; 1199 | b._pack_prev = a; 1200 | } 1201 | 1202 | function d3_layout_packIntersects(a, b) { 1203 | var dx = b.x - a.x, 1204 | dy = b.y - a.y, 1205 | dr = a.r + b.r; 1206 | return dr * dr - dx * dx - dy * dy > .001; // within epsilon 1207 | } 1208 | 1209 | function d3_layout_packCircle(nodes) { 1210 | var xMin = Infinity, 1211 | xMax = -Infinity, 1212 | yMin = Infinity, 1213 | yMax = -Infinity, 1214 | n = nodes.length, 1215 | a, b, c, j, k; 1216 | 1217 | function bound(node) { 1218 | xMin = Math.min(node.x - node.r, xMin); 1219 | xMax = Math.max(node.x + node.r, xMax); 1220 | yMin = Math.min(node.y - node.r, yMin); 1221 | yMax = Math.max(node.y + node.r, yMax); 1222 | } 1223 | 1224 | // Create node links. 1225 | nodes.forEach(d3_layout_packLink); 1226 | 1227 | // Create first node. 1228 | a = nodes[0]; 1229 | a.x = -a.r; 1230 | a.y = 0; 1231 | bound(a); 1232 | 1233 | // Create second node. 1234 | if (n > 1) { 1235 | b = nodes[1]; 1236 | b.x = b.r; 1237 | b.y = 0; 1238 | bound(b); 1239 | 1240 | // Create third node and build chain. 1241 | if (n > 2) { 1242 | c = nodes[2]; 1243 | d3_layout_packPlace(a, b, c); 1244 | bound(c); 1245 | d3_layout_packInsert(a, c); 1246 | a._pack_prev = c; 1247 | d3_layout_packInsert(c, b); 1248 | b = a._pack_next; 1249 | 1250 | // Now iterate through the rest. 1251 | for (var i = 3; i < n; i++) { 1252 | d3_layout_packPlace(a, b, c = nodes[i]); 1253 | 1254 | // Search for the closest intersection. 1255 | var isect = 0, s1 = 1, s2 = 1; 1256 | for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { 1257 | if (d3_layout_packIntersects(j, c)) { 1258 | isect = 1; 1259 | break; 1260 | } 1261 | } 1262 | if (isect == 1) { 1263 | for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { 1264 | if (d3_layout_packIntersects(k, c)) { 1265 | break; 1266 | } 1267 | } 1268 | } 1269 | 1270 | // Update node chain. 1271 | if (isect) { 1272 | if (s1 < s2 || (s1 == s2 && b.r < a.r)) d3_layout_packSplice(a, b = j); 1273 | else d3_layout_packSplice(a = k, b); 1274 | i--; 1275 | } else { 1276 | d3_layout_packInsert(a, c); 1277 | b = c; 1278 | bound(c); 1279 | } 1280 | } 1281 | } 1282 | } 1283 | 1284 | // Re-center the circles and return the encompassing radius. 1285 | var cx = (xMin + xMax) / 2, 1286 | cy = (yMin + yMax) / 2, 1287 | cr = 0; 1288 | for (var i = 0; i < n; i++) { 1289 | var node = nodes[i]; 1290 | node.x -= cx; 1291 | node.y -= cy; 1292 | cr = Math.max(cr, node.r + Math.sqrt(node.x * node.x + node.y * node.y)); 1293 | } 1294 | 1295 | // Remove node links. 1296 | nodes.forEach(d3_layout_packUnlink); 1297 | 1298 | return cr; 1299 | } 1300 | 1301 | function d3_layout_packLink(node) { 1302 | node._pack_next = node._pack_prev = node; 1303 | } 1304 | 1305 | function d3_layout_packUnlink(node) { 1306 | delete node._pack_next; 1307 | delete node._pack_prev; 1308 | } 1309 | 1310 | function d3_layout_packTree(node) { 1311 | var children = node.children; 1312 | if (children && children.length) { 1313 | children.forEach(d3_layout_packTree); 1314 | node.r = d3_layout_packCircle(children); 1315 | } else { 1316 | node.r = Math.sqrt(node.value); 1317 | } 1318 | } 1319 | 1320 | function d3_layout_packTransform(node, x, y, k) { 1321 | var children = node.children; 1322 | node.x = (x += k * node.x); 1323 | node.y = (y += k * node.y); 1324 | node.r *= k; 1325 | if (children) { 1326 | var i = -1, n = children.length; 1327 | while (++i < n) d3_layout_packTransform(children[i], x, y, k); 1328 | } 1329 | } 1330 | 1331 | function d3_layout_packPlace(a, b, c) { 1332 | var db = a.r + c.r, 1333 | dx = b.x - a.x, 1334 | dy = b.y - a.y; 1335 | if (db && (dx || dy)) { 1336 | var da = b.r + c.r, 1337 | dc = Math.sqrt(dx * dx + dy * dy), 1338 | cos = Math.max(-1, Math.min(1, (db * db + dc * dc - da * da) / (2 * db * dc))), 1339 | theta = Math.acos(cos), 1340 | x = cos * (db /= dc), 1341 | y = Math.sin(theta) * db; 1342 | c.x = a.x + x * dx + y * dy; 1343 | c.y = a.y + x * dy - y * dx; 1344 | } else { 1345 | c.x = a.x + db; 1346 | c.y = a.y; 1347 | } 1348 | } 1349 | // Implements a hierarchical layout using the cluster (or dendogram) algorithm. 1350 | d3.layout.cluster = function() { 1351 | var hierarchy = d3.layout.hierarchy().sort(null).value(null), 1352 | separation = d3_layout_treeSeparation, 1353 | size = [1, 1]; // width, height 1354 | 1355 | function cluster(d, i) { 1356 | var nodes = hierarchy.call(this, d, i), 1357 | root = nodes[0], 1358 | previousNode, 1359 | x = 0, 1360 | kx, 1361 | ky; 1362 | 1363 | // First walk, computing the initial x & y values. 1364 | d3_layout_treeVisitAfter(root, function(node) { 1365 | var children = node.children; 1366 | if (children && children.length) { 1367 | node.x = d3_layout_clusterX(children); 1368 | node.y = d3_layout_clusterY(children); 1369 | } else { 1370 | node.x = previousNode ? x += separation(node, previousNode) : 0; 1371 | node.y = 0; 1372 | previousNode = node; 1373 | } 1374 | }); 1375 | 1376 | // Compute the left-most, right-most, and depth-most nodes for extents. 1377 | var left = d3_layout_clusterLeft(root), 1378 | right = d3_layout_clusterRight(root), 1379 | x0 = left.x - separation(left, right) / 2, 1380 | x1 = right.x + separation(right, left) / 2; 1381 | 1382 | // Second walk, normalizing x & y to the desired size. 1383 | d3_layout_treeVisitAfter(root, function(node) { 1384 | node.x = (node.x - x0) / (x1 - x0) * size[0]; 1385 | node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1]; 1386 | }); 1387 | 1388 | return nodes; 1389 | } 1390 | 1391 | cluster.separation = function(x) { 1392 | if (!arguments.length) return separation; 1393 | separation = x; 1394 | return cluster; 1395 | }; 1396 | 1397 | cluster.size = function(x) { 1398 | if (!arguments.length) return size; 1399 | size = x; 1400 | return cluster; 1401 | }; 1402 | 1403 | return d3_layout_hierarchyRebind(cluster, hierarchy); 1404 | }; 1405 | 1406 | function d3_layout_clusterY(children) { 1407 | return 1 + d3.max(children, function(child) { 1408 | return child.y; 1409 | }); 1410 | } 1411 | 1412 | function d3_layout_clusterX(children) { 1413 | return children.reduce(function(x, child) { 1414 | return x + child.x; 1415 | }, 0) / children.length; 1416 | } 1417 | 1418 | function d3_layout_clusterLeft(node) { 1419 | var children = node.children; 1420 | return children && children.length ? d3_layout_clusterLeft(children[0]) : node; 1421 | } 1422 | 1423 | function d3_layout_clusterRight(node) { 1424 | var children = node.children, n; 1425 | return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; 1426 | } 1427 | // Node-link tree diagram using the Reingold-Tilford "tidy" algorithm 1428 | d3.layout.tree = function() { 1429 | var hierarchy = d3.layout.hierarchy().sort(null).value(null), 1430 | separation = d3_layout_treeSeparation, 1431 | size = [1, 1]; // width, height 1432 | 1433 | function tree(d, i) { 1434 | var nodes = hierarchy.call(this, d, i), 1435 | root = nodes[0]; 1436 | 1437 | function firstWalk(node, previousSibling) { 1438 | var children = node.children, 1439 | layout = node._tree; 1440 | if (children && (n = children.length)) { 1441 | var n, 1442 | firstChild = children[0], 1443 | previousChild, 1444 | ancestor = firstChild, 1445 | child, 1446 | i = -1; 1447 | while (++i < n) { 1448 | child = children[i]; 1449 | firstWalk(child, previousChild); 1450 | ancestor = apportion(child, previousChild, ancestor); 1451 | previousChild = child; 1452 | } 1453 | d3_layout_treeShift(node); 1454 | var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim); 1455 | if (previousSibling) { 1456 | layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling); 1457 | layout.mod = layout.prelim - midpoint; 1458 | } else { 1459 | layout.prelim = midpoint; 1460 | } 1461 | } else { 1462 | if (previousSibling) { 1463 | layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling); 1464 | } 1465 | } 1466 | } 1467 | 1468 | function secondWalk(node, x) { 1469 | node.x = node._tree.prelim + x; 1470 | var children = node.children; 1471 | if (children && (n = children.length)) { 1472 | var i = -1, 1473 | n; 1474 | x += node._tree.mod; 1475 | while (++i < n) { 1476 | secondWalk(children[i], x); 1477 | } 1478 | } 1479 | } 1480 | 1481 | function apportion(node, previousSibling, ancestor) { 1482 | if (previousSibling) { 1483 | var vip = node, 1484 | vop = node, 1485 | vim = previousSibling, 1486 | vom = node.parent.children[0], 1487 | sip = vip._tree.mod, 1488 | sop = vop._tree.mod, 1489 | sim = vim._tree.mod, 1490 | som = vom._tree.mod, 1491 | shift; 1492 | while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { 1493 | vom = d3_layout_treeLeft(vom); 1494 | vop = d3_layout_treeRight(vop); 1495 | vop._tree.ancestor = node; 1496 | shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip); 1497 | if (shift > 0) { 1498 | d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift); 1499 | sip += shift; 1500 | sop += shift; 1501 | } 1502 | sim += vim._tree.mod; 1503 | sip += vip._tree.mod; 1504 | som += vom._tree.mod; 1505 | sop += vop._tree.mod; 1506 | } 1507 | if (vim && !d3_layout_treeRight(vop)) { 1508 | vop._tree.thread = vim; 1509 | vop._tree.mod += sim - sop; 1510 | } 1511 | if (vip && !d3_layout_treeLeft(vom)) { 1512 | vom._tree.thread = vip; 1513 | vom._tree.mod += sip - som; 1514 | ancestor = node; 1515 | } 1516 | } 1517 | return ancestor; 1518 | } 1519 | 1520 | // Initialize temporary layout variables. 1521 | d3_layout_treeVisitAfter(root, function(node, previousSibling) { 1522 | node._tree = { 1523 | ancestor: node, 1524 | prelim: 0, 1525 | mod: 0, 1526 | change: 0, 1527 | shift: 0, 1528 | number: previousSibling ? previousSibling._tree.number + 1 : 0 1529 | }; 1530 | }); 1531 | 1532 | // Compute the layout using Buchheim et al.'s algorithm. 1533 | firstWalk(root); 1534 | secondWalk(root, -root._tree.prelim); 1535 | 1536 | // Compute the left-most, right-most, and depth-most nodes for extents. 1537 | var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), 1538 | right = d3_layout_treeSearch(root, d3_layout_treeRightmost), 1539 | deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), 1540 | x0 = left.x - separation(left, right) / 2, 1541 | x1 = right.x + separation(right, left) / 2, 1542 | y1 = deep.depth || 1; 1543 | 1544 | // Clear temporary layout variables; transform x and y. 1545 | d3_layout_treeVisitAfter(root, function(node) { 1546 | node.x = (node.x - x0) / (x1 - x0) * size[0]; 1547 | node.y = node.depth / y1 * size[1]; 1548 | delete node._tree; 1549 | }); 1550 | 1551 | return nodes; 1552 | } 1553 | 1554 | tree.separation = function(x) { 1555 | if (!arguments.length) return separation; 1556 | separation = x; 1557 | return tree; 1558 | }; 1559 | 1560 | tree.size = function(x) { 1561 | if (!arguments.length) return size; 1562 | size = x; 1563 | return tree; 1564 | }; 1565 | 1566 | return d3_layout_hierarchyRebind(tree, hierarchy); 1567 | }; 1568 | 1569 | function d3_layout_treeSeparation(a, b) { 1570 | return a.parent == b.parent ? 1 : 2; 1571 | } 1572 | 1573 | // function d3_layout_treeSeparationRadial(a, b) { 1574 | // return (a.parent == b.parent ? 1 : 2) / a.depth; 1575 | // } 1576 | 1577 | function d3_layout_treeLeft(node) { 1578 | var children = node.children; 1579 | return children && children.length ? children[0] : node._tree.thread; 1580 | } 1581 | 1582 | function d3_layout_treeRight(node) { 1583 | var children = node.children, 1584 | n; 1585 | return children && (n = children.length) ? children[n - 1] : node._tree.thread; 1586 | } 1587 | 1588 | function d3_layout_treeSearch(node, compare) { 1589 | var children = node.children; 1590 | if (children && (n = children.length)) { 1591 | var child, 1592 | n, 1593 | i = -1; 1594 | while (++i < n) { 1595 | if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) { 1596 | node = child; 1597 | } 1598 | } 1599 | } 1600 | return node; 1601 | } 1602 | 1603 | function d3_layout_treeRightmost(a, b) { 1604 | return a.x - b.x; 1605 | } 1606 | 1607 | function d3_layout_treeLeftmost(a, b) { 1608 | return b.x - a.x; 1609 | } 1610 | 1611 | function d3_layout_treeDeepest(a, b) { 1612 | return a.depth - b.depth; 1613 | } 1614 | 1615 | function d3_layout_treeVisitAfter(node, callback) { 1616 | function visit(node, previousSibling) { 1617 | var children = node.children; 1618 | if (children && (n = children.length)) { 1619 | var child, 1620 | previousChild = null, 1621 | i = -1, 1622 | n; 1623 | while (++i < n) { 1624 | child = children[i]; 1625 | visit(child, previousChild); 1626 | previousChild = child; 1627 | } 1628 | } 1629 | callback(node, previousSibling); 1630 | } 1631 | visit(node, null); 1632 | } 1633 | 1634 | function d3_layout_treeShift(node) { 1635 | var shift = 0, 1636 | change = 0, 1637 | children = node.children, 1638 | i = children.length, 1639 | child; 1640 | while (--i >= 0) { 1641 | child = children[i]._tree; 1642 | child.prelim += shift; 1643 | child.mod += shift; 1644 | shift += child.shift + (change += child.change); 1645 | } 1646 | } 1647 | 1648 | function d3_layout_treeMove(ancestor, node, shift) { 1649 | ancestor = ancestor._tree; 1650 | node = node._tree; 1651 | var change = shift / (node.number - ancestor.number); 1652 | ancestor.change += change; 1653 | node.change -= change; 1654 | node.shift += shift; 1655 | node.prelim += shift; 1656 | node.mod += shift; 1657 | } 1658 | 1659 | function d3_layout_treeAncestor(vim, node, ancestor) { 1660 | return vim._tree.ancestor.parent == node.parent 1661 | ? vim._tree.ancestor 1662 | : ancestor; 1663 | } 1664 | // Squarified Treemaps by Mark Bruls, Kees Huizing, and Jarke J. van Wijk 1665 | // Modified to support a target aspect ratio by Jeff Heer 1666 | d3.layout.treemap = function() { 1667 | var hierarchy = d3.layout.hierarchy(), 1668 | round = Math.round, 1669 | size = [1, 1], // width, height 1670 | padding = null, 1671 | pad = d3_layout_treemapPadNull, 1672 | sticky = false, 1673 | stickies, 1674 | ratio = 0.5 * (1 + Math.sqrt(5)); // golden ratio 1675 | 1676 | // Compute the area for each child based on value & scale. 1677 | function scale(children, k) { 1678 | var i = -1, 1679 | n = children.length, 1680 | child, 1681 | area; 1682 | while (++i < n) { 1683 | area = (child = children[i]).value * (k < 0 ? 0 : k); 1684 | child.area = isNaN(area) || area <= 0 ? 0 : area; 1685 | } 1686 | } 1687 | 1688 | // Recursively arranges the specified node's children into squarified rows. 1689 | function squarify(node) { 1690 | var children = node.children; 1691 | if (children && children.length) { 1692 | var rect = pad(node), 1693 | row = [], 1694 | remaining = children.slice(), // copy-on-write 1695 | child, 1696 | best = Infinity, // the best row score so far 1697 | score, // the current row score 1698 | u = Math.min(rect.dx, rect.dy), // initial orientation 1699 | n; 1700 | scale(remaining, rect.dx * rect.dy / node.value); 1701 | row.area = 0; 1702 | while ((n = remaining.length) > 0) { 1703 | row.push(child = remaining[n - 1]); 1704 | row.area += child.area; 1705 | if ((score = worst(row, u)) <= best) { // continue with this orientation 1706 | remaining.pop(); 1707 | best = score; 1708 | } else { // abort, and try a different orientation 1709 | row.area -= row.pop().area; 1710 | position(row, u, rect, false); 1711 | u = Math.min(rect.dx, rect.dy); 1712 | row.length = row.area = 0; 1713 | best = Infinity; 1714 | } 1715 | } 1716 | if (row.length) { 1717 | position(row, u, rect, true); 1718 | row.length = row.area = 0; 1719 | } 1720 | children.forEach(squarify); 1721 | } 1722 | } 1723 | 1724 | // Recursively resizes the specified node's children into existing rows. 1725 | // Preserves the existing layout! 1726 | function stickify(node) { 1727 | var children = node.children; 1728 | if (children && children.length) { 1729 | var rect = pad(node), 1730 | remaining = children.slice(), // copy-on-write 1731 | child, 1732 | row = []; 1733 | scale(remaining, rect.dx * rect.dy / node.value); 1734 | row.area = 0; 1735 | while (child = remaining.pop()) { 1736 | row.push(child); 1737 | row.area += child.area; 1738 | if (child.z != null) { 1739 | position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); 1740 | row.length = row.area = 0; 1741 | } 1742 | } 1743 | children.forEach(stickify); 1744 | } 1745 | } 1746 | 1747 | // Computes the score for the specified row, as the worst aspect ratio. 1748 | function worst(row, u) { 1749 | var s = row.area, 1750 | r, 1751 | rmax = 0, 1752 | rmin = Infinity, 1753 | i = -1, 1754 | n = row.length; 1755 | while (++i < n) { 1756 | if (!(r = row[i].area)) continue; 1757 | if (r < rmin) rmin = r; 1758 | if (r > rmax) rmax = r; 1759 | } 1760 | s *= s; 1761 | u *= u; 1762 | return s 1763 | ? Math.max((u * rmax * ratio) / s, s / (u * rmin * ratio)) 1764 | : Infinity; 1765 | } 1766 | 1767 | // Positions the specified row of nodes. Modifies `rect`. 1768 | function position(row, u, rect, flush) { 1769 | var i = -1, 1770 | n = row.length, 1771 | x = rect.x, 1772 | y = rect.y, 1773 | v = u ? round(row.area / u) : 0, 1774 | o; 1775 | if (u == rect.dx) { // horizontal subdivision 1776 | if (flush || v > rect.dy) v = v ? rect.dy : 0; // over+underflow 1777 | while (++i < n) { 1778 | o = row[i]; 1779 | o.x = x; 1780 | o.y = y; 1781 | o.dy = v; 1782 | x += o.dx = v ? round(o.area / v) : 0; 1783 | } 1784 | o.z = true; 1785 | o.dx += rect.x + rect.dx - x; // rounding error 1786 | rect.y += v; 1787 | rect.dy -= v; 1788 | } else { // vertical subdivision 1789 | if (flush || v > rect.dx) v = v ? rect.dx : 0; // over+underflow 1790 | while (++i < n) { 1791 | o = row[i]; 1792 | o.x = x; 1793 | o.y = y; 1794 | o.dx = v; 1795 | y += o.dy = v ? round(o.area / v) : 0; 1796 | } 1797 | o.z = false; 1798 | o.dy += rect.y + rect.dy - y; // rounding error 1799 | rect.x += v; 1800 | rect.dx -= v; 1801 | } 1802 | } 1803 | 1804 | function treemap(d) { 1805 | var nodes = stickies || hierarchy(d), 1806 | root = nodes[0]; 1807 | root.x = 0; 1808 | root.y = 0; 1809 | root.dx = size[0]; 1810 | root.dy = size[1]; 1811 | if (stickies) hierarchy.revalue(root); 1812 | scale([root], root.dx * root.dy / root.value); 1813 | (stickies ? stickify : squarify)(root); 1814 | if (sticky) stickies = nodes; 1815 | return nodes; 1816 | } 1817 | 1818 | treemap.size = function(x) { 1819 | if (!arguments.length) return size; 1820 | size = x; 1821 | return treemap; 1822 | }; 1823 | 1824 | treemap.padding = function(x) { 1825 | if (!arguments.length) return padding; 1826 | 1827 | function padFunction(node) { 1828 | var p = x.call(treemap, node, node.depth); 1829 | return p == null 1830 | ? d3_layout_treemapPadNull(node) 1831 | : d3_layout_treemapPad(node, typeof p === "number" ? [p, p, p, p] : p); 1832 | } 1833 | 1834 | function padConstant(node) { 1835 | return d3_layout_treemapPad(node, x); 1836 | } 1837 | 1838 | var type; 1839 | pad = (padding = x) == null ? d3_layout_treemapPadNull 1840 | : (type = typeof x) === "function" ? padFunction 1841 | : type === "number" ? (x = [x, x, x, x], padConstant) 1842 | : padConstant; 1843 | return treemap; 1844 | }; 1845 | 1846 | treemap.round = function(x) { 1847 | if (!arguments.length) return round != Number; 1848 | round = x ? Math.round : Number; 1849 | return treemap; 1850 | }; 1851 | 1852 | treemap.sticky = function(x) { 1853 | if (!arguments.length) return sticky; 1854 | sticky = x; 1855 | stickies = null; 1856 | return treemap; 1857 | }; 1858 | 1859 | treemap.ratio = function(x) { 1860 | if (!arguments.length) return ratio; 1861 | ratio = x; 1862 | return treemap; 1863 | }; 1864 | 1865 | return d3_layout_hierarchyRebind(treemap, hierarchy); 1866 | }; 1867 | 1868 | function d3_layout_treemapPadNull(node) { 1869 | return {x: node.x, y: node.y, dx: node.dx, dy: node.dy}; 1870 | } 1871 | 1872 | function d3_layout_treemapPad(node, padding) { 1873 | var x = node.x + padding[3], 1874 | y = node.y + padding[0], 1875 | dx = node.dx - padding[1] - padding[3], 1876 | dy = node.dy - padding[0] - padding[2]; 1877 | if (dx < 0) { x += dx / 2; dx = 0; } 1878 | if (dy < 0) { y += dy / 2; dy = 0; } 1879 | return {x: x, y: y, dx: dx, dy: dy}; 1880 | } 1881 | })(); 1882 | -------------------------------------------------------------------------------- /skosxl/static/d3.tree.css: -------------------------------------------------------------------------------- 1 | /* @override http://localhost:8000/static/d3.tree.css */ 2 | 3 | .gallery { 4 | background-color: #f8fcff; 5 | padding: 15px; 6 | } 7 | 8 | .node circle { 9 | fill: #f9fccb; 10 | stroke: #f19964; 11 | stroke-width: 1.2px; 12 | color: #f19964; 13 | cursor: pointer; 14 | } 15 | 16 | .node { 17 | font: 13px "PT Sans Narrow", "Trebuchet MS", Trebuchet, Tahoma, Arial; 18 | cursor: pointer; 19 | 20 | } 21 | 22 | .link { 23 | fill: none; 24 | stroke: #ccc; 25 | stroke-width: 1.5px; 26 | } 27 | -------------------------------------------------------------------------------- /skosxl/static/d3.tree.js: -------------------------------------------------------------------------------- 1 | var r = 960 / 2; 2 | 3 | var tree = d3.layout.tree() 4 | .size([180, r -120]) 5 | .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; }); 6 | 7 | var diagonal = d3.svg.diagonal.radial() 8 | .projection(function(d) { return [d.y, d.x / 180 * Math.PI]; }); 9 | 10 | var vis = d3.select("#chart").append("svg") 11 | .attr("width", r * 2) 12 | .attr("height", r * 2 - 150) 13 | .append("g") 14 | .attr("transform", "translate(" + r + "," + r + ")"); 15 | 16 | d3.json(json_data, function(json) { 17 | var nodes = tree.nodes(json); 18 | 19 | var link = vis.selectAll("path.link") 20 | .data(tree.links(nodes)) 21 | .enter().append("path") 22 | .attr("class", "link") 23 | .attr("d", diagonal); 24 | 25 | var node = vis.selectAll("g.node") 26 | .data(nodes) 27 | .enter().append("g") 28 | .attr("class", "node") 29 | .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; }) 30 | .on("click", function(d,i) { 31 | window.location.href = d.url ; 32 | }) 33 | 34 | node.append("circle") 35 | .attr("r", 4.5); 36 | 37 | node.append("title") 38 | .text(function(d) { return d.name; }); 39 | 40 | node.append("text") 41 | .attr("dx", function(d) { return d.x < 180 ? 8 : -8; }) 42 | .attr("dy", ".5em") 43 | .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) 44 | .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }) 45 | .text(function(d) { return d.name; }); 46 | }); 47 | -------------------------------------------------------------------------------- /skosxl/templates/admin/autocomplete/fk_searchinput.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {% trans 5 | 6 | -------------------------------------------------------------------------------- /skosxl/templates/admin/autocomplete/inline_searchinput.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {% trans 5 | 6 | -------------------------------------------------------------------------------- /skosxl/templates/admin/autocomplete/nolookups_foreignkey_searchinput.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {# #} 4 | {# {% trans #} 5 | {# #} 6 | -------------------------------------------------------------------------------- /skosxl/templates/admin_concept_change.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_modify %} 3 | {% load staticfiles %} 4 | {% load url from future %} 5 | 6 | {% block extrahead %}{{ block.super }} 7 | {% url 'admin:jsi18n' as jsi18nurl %} 8 | 9 | {{ media }} 10 | {% endblock %} 11 | 12 | {% block extrastyle %}{{ block.super }}{% endblock %} 13 | 14 | {% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %} 15 | 16 | {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} 17 | 18 | {% block breadcrumbs %}{% if not is_popup %} 19 | 25 | {% endif %}{% endblock %} 26 | 27 | 28 | {% block content_title %} 29 | {% if original %} 30 |

{% trans "View on site" %} {{ adminform.form.instance }}

31 | {% else %} 32 |

{{ title }}

33 | {% endif %} 34 |
35 | {% endblock %} 36 | 37 | 38 | {% block content %} 39 | 40 |
41 | {% block object-tools %} 42 | {% if change %}{% if not is_popup %} 43 | 49 | {% endif %}{% endif %} 50 | {% endblock %} 51 |
{% csrf_token %}{% block form_top %}{% endblock %} 52 |
53 | {% if is_popup %}{% endif %} 54 | {% if save_on_top %}{% submit_row %}{% endif %} 55 | {% if errors %} 56 |

57 | {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 58 |

59 | {{ adminform.form.non_field_errors }} 60 | {% endif %} 61 | 62 | {% for fieldset in adminform %} 63 | {% include "admin/includes/fieldset.html" %} 64 | {% endfor %} 65 | 66 | {% block after_field_sets %}{% endblock %} 67 | 68 | {% for inline_admin_formset in inline_admin_formsets %} 69 | {% include inline_admin_formset.opts.template %} 70 | {% endfor %} 71 | 72 | {% block after_related_objects %} 73 | 74 |
75 |

{% trans "Mapping relations" %}

76 |
77 | {% for list in lists %} 78 | {{list.name}} 83 | {% endfor %} 84 |
85 |
86 | 87 | {% endblock %} 88 | 89 | {% submit_row %} 90 | 91 | {% if adminform and add %} 92 | 93 | {% endif %} 94 | 95 | {# JavaScript for prepopulated fields #} 96 | {% prepopulated_fields_js %} 97 | 98 |
99 |
100 | 101 | 102 | 103 | 104 | {% endblock %} 105 | -------------------------------------------------------------------------------- /skosxl/templates/admin_concept_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% block content %} 3 | 4 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | {{ block.super }} 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /skosxl/templates/concept_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block meta_title %}Concept : {{object}}{% endblock %} 4 | 5 | {% block document %} 6 | 7 |
8 | {% block title %}

Concept : {{object}}

{% endblock %} 9 |

Ajouté le {{object.created|date:'l t F Y'}} par {{object.user}}

10 |
11 |
12 | 13 | {% block content %} 14 |
15 |
16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | {% endblock %} 25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /skosxl/templates/scheme_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block meta_title %}Scheme : {{object}}{% endblock %} 4 | 5 | {% block document %} 6 | 7 |
8 | {% block title %}

SKOS Scheme : {{object}}

{% endblock %} 9 |

Ajouté le {{object.created|date:'l t F Y'}} par {{object.user}}

10 |
11 |
12 | 13 | {% block content %} 14 |
15 |
16 | 17 | 18 | {% if concepts %} 19 |

Concept

20 |
    21 | {% for c in concepts %} 22 |
  • {{ c }}
  • 23 | {%endfor%} 24 |
25 |
26 | {% endif %} 27 |
28 | 29 | 30 | 31 |
32 | {% endblock %} 33 | {% endblock %} 34 | 35 | -------------------------------------------------------------------------------- /skosxl/templates/tag_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block meta_title %}Tag : {{object}}{% endblock %} 4 | 5 | {% block document %} 6 | 7 |
8 | {% block title %}

Tag : {{object}}

{% endblock %} 9 |

Ajouté le {{object.created|date:'l t F Y'}} par {{object.user}}

10 |
11 |
12 | 13 | {% block content %} 14 |
15 |
16 | 17 | 18 | {% if initiatives %} 19 |

Initiatives

20 |
    21 | {% for i in initiatives %} 22 |
  • {{ i.title }}
  • 23 | {%endfor%} 24 |
25 |
26 | {% endif %} 27 |
28 | 29 | 30 | 31 |
32 | {% endblock %} 33 | {% endblock %} 34 | 35 | -------------------------------------------------------------------------------- /skosxl/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | #from django.conf.urls.defaults import * 3 | from django.conf.urls import * 4 | 5 | urlpatterns = patterns('skosxl.views', 6 | 7 | url(r'^admin_scheme_tree/(?P\d+)/$','json_scheme_tree',{'admin_url': True},'json_admin_scheme_tree'), 8 | url(r'^json_scheme_tree/(?P\d+)/$','json_scheme_tree', {'admin_url': False},'json_scheme_tree'), 9 | 10 | url(r'^scheme/(?P[\w-]+)/$', 'scheme_detail', name="scheme_detail"), 11 | url(r'^concept/(?P\d+)/$', 'concept_detail', name="concept_detail"), 12 | url(r'^label/(?P[\w-]+)/$', 'tag_detail', name="tag_detail"), 13 | 14 | url(r'^sparql/$', 'sparql_query', name="sparql_query") 15 | 16 | 17 | ) -------------------------------------------------------------------------------- /skosxl/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-rdf-tools/django-skosxl/5636e0474cc6dcfe48cc6187dcbdfa52ab252d03/skosxl/utils/__init__.py -------------------------------------------------------------------------------- /skosxl/utils/autocomplete_admin.py: -------------------------------------------------------------------------------- 1 | # 2 | # Autocomplete feature for admin panel 3 | # 4 | # Most of the code has been written by Jannis Leidel, then updated a bit 5 | # for django_extensions, finally reassembled and extended by Mikele Pasin. 6 | # 7 | # http://jannisleidel.com/2008/11/autocomplete-form-widget-foreignkey-model-fields/ 8 | # http://code.google.com/p/django-command-extensions/ 9 | # http://magicrebirth.wordpress.com/ 10 | # 11 | # to_string_function, Satchmo adaptation and some comments added by emes 12 | # (Michal Salaban) 13 | # 14 | 15 | 16 | 17 | 18 | # ============== 19 | # HOW-TO 20 | # ============== 21 | 22 | # 1.Put this file somewhere in your application folder 23 | 24 | # 2.Add the 'autocomplete' folder with the media files to your usual media folder 25 | 26 | # 3.Add the 'admin/autocomplete' folder to your templates folder 27 | 28 | # 4.Add the extrafilters.py file in the templatetags directory of your application (or just add its contents to 29 | # your custom template tags if you already have some). 30 | # All is needed is the 'cut' filter, for making the code used in the inline-autocomplete form javascript friendly 31 | 32 | # 5. When defining your models admin, import the relevant admin and use it: 33 | # ..... 34 | # from myproject.mypackage.autocomplete_admin import FkAutocompleteAdmin 35 | # ..... 36 | # ..... 37 | # class Admin (FkAutocompleteAdmin): 38 | # related_search_fields = { 'person': ('name',)} 39 | # ..... 40 | 41 | 42 | # ============== 43 | # TROUBLE SHOOTING: 44 | # ============== 45 | 46 | # ** sometimes things don't work cause you have to add 'from django.conf.urls.defaults import *' to the modules where you use the autocomplete 47 | # ** if you're using the inline-autocompletion, make sure that the main admin class the inline belong to is a subclass FkAutocompleteAdmin 48 | # ** you may need to hack it a bit to make it work for you - it's been done in a rush! 49 | 50 | 51 | # ============== 52 | # the code now.... 53 | # ============== 54 | 55 | 56 | from django import forms 57 | from django.conf import settings 58 | from django.utils.safestring import mark_safe 59 | # remove deprecated - now -dead method 60 | #from django.utils.text import truncate_words 61 | from django.utils.text import Truncator 62 | from django.template.loader import render_to_string 63 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget 64 | import operator 65 | from django.http import HttpResponse, HttpResponseNotFound 66 | from django.contrib import admin 67 | from django.db import models 68 | from django.db.models.query import QuerySet 69 | from django.utils.encoding import smart_str 70 | from django.utils.translation import ugettext as _ 71 | from django.utils.text import get_text_list 72 | # added by mikele 73 | # from django.conf.urls.defaults import * 74 | from django.conf.urls import patterns 75 | from django.contrib.admin.sites import site 76 | 77 | 78 | 79 | class FkSearchInput(ForeignKeyRawIdWidget): 80 | """ 81 | A Widget for displaying ForeignKeys in an autocomplete search input 82 | instead in a box. 162 | """ 163 | 164 | # Set in subclass to render the widget with a different template 165 | widget_template = None 166 | # Set this to the patch of the search view 167 | search_path = '../foreignkey_autocomplete/' 168 | 169 | class Media: 170 | css = { 171 | 'all': ('autocomplete/css/jquery.autocomplete.css',) 172 | } 173 | js = ( 174 | #'autocomplete/js/jquery.js', 175 | 'autocomplete/js/jquery.bgiframe.min.js', 176 | 'autocomplete/js/jquery.ajaxQueue.js', 177 | 'autocomplete/js/jquery.autocomplete.js', 178 | ) 179 | 180 | def label_for_value(self, value): 181 | key = self.rel.get_related_field().name 182 | obj = self.rel.to._default_manager.get(**{key: value}) 183 | return Truncate(obj).words(14) 184 | 185 | def __init__(self, rel, search_fields, attrs=None, admin_site=site): 186 | self.search_fields = search_fields 187 | self.admin_site = site 188 | super(NoLookupsForeignKeySearchInput, self).__init__(rel, admin_site, attrs) 189 | 190 | def render(self, name, value, attrs=None): 191 | if attrs is None: 192 | attrs = {} 193 | output = [super(NoLookupsForeignKeySearchInput, self).render(name, value, attrs)] 194 | opts = self.rel.to._meta 195 | app_label = opts.app_label 196 | model_name = opts.object_name.lower() 197 | related_url = '../../../%s/%s/' % (app_label, model_name) 198 | params = self.url_parameters() 199 | if params: 200 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) 201 | else: 202 | url = '' 203 | if not attrs.has_key('class'): 204 | attrs['class'] = 'vForeignKeyRawIdAdminField' 205 | # Call the TextInput render method directly to have more control 206 | output = [forms.TextInput.render(self, name, value, attrs)] 207 | if value: 208 | label = self.label_for_value(value) 209 | else: 210 | label = u'' 211 | context = { 212 | 'url': url, 213 | 'related_url': related_url, 214 | 'admin_media_prefix': settings.STATIC_URL, 215 | 'search_path': self.search_path, 216 | 'search_fields': ','.join(self.search_fields), 217 | 'model_name': model_name, 218 | 'app_label': app_label, 219 | 'label': label, 220 | 'name': name, 221 | } 222 | 223 | # tried to change widget_template but it needs a subclass (of what?) not a string 224 | 225 | output.append(render_to_string(self.widget_template or ( 226 | '%s/%s/foreignkey_searchinput.html' % (app_label, model_name), 227 | '%s/foreignkey_searchinput.html' % app_label, 228 | 'admin/autocomplete/nolookups_foreignkey_searchinput.html', 229 | ), context)) 230 | output.reverse() 231 | return mark_safe(u''.join(output)) 232 | 233 | 234 | 235 | class InlineSearchInput(ForeignKeyRawIdWidget): 236 | """ 237 | A Widget for displaying ForeignKeys in an autocomplete search input 238 | instead in a