geovistory / design-system

MIT License
2 stars 1 forks source link

Map with timeline #116

Open joschne opened 9 months ago

joschne commented 9 months ago

Problem description

The current map plugin is great for visualization of a static map.

Since Geovistory deals with historical data, time is an important aspect. In order to show developments of historical phenomena on a map, a map with time filter and time line would be great.

Functional requirement

The time-map-with-circles has to deal with this kind of input data (example): pointUri (uri) pointLabel (xsd:string) eventUri (uri) eventLabel (xsd:string) long (xsd:float) lat (xsd:float) start (xsd:date) end (xsd:date)  type (xsd:string)
<geoplace1> Bern <eventA> Laura in Bern 12.3 56.7 1597-02-25  1597-08-11  Work
<geoplace1> Bern <eventB> Max in Bern 12.3 56.7 1599-01-12  1599-07-19  Work
<geoplace2> Lyon <eventA> Jean in Lyon 12.3 56.7 1600-01-21  1600-01-21  Voyage
<geoplace2> Lyon <eventA> Maria in Lyon 12.3 56.7 1600-01-01  1601-01-01  Work

This example data can be interpreted so: Laura worked a few months in Bern in 1597, Max in 1599. Jean travelled through Lyon the January 21st of 1600. Maria worked one year in Lyon in 1600.

The long and lat are geographic coordinates; start and end define the event-time-interval.

The component has these functional requirements:

With the above example data and a time-interval-filter of 1590–1610 (showing all 4 records), there would be

[EDIT after discussion below]

The map displays one circle per pointURI and type. This means, we group the events per pointURI and type. Each group will be displayed as a circle. The circle radius is proportional to the number of events per group, the color depends on the type.

More technically, each group of pointURI and type aggregates the event attributes in a list: start, end, eventLabel, eventURI and picks one pointLabel, lat, long for each group. The output data structure could look like this (pseudo code):

{
  "<geoplace1>_Work": {
    "group": {
      "pointURI": "<geoplace1>",
      "pointLabel": "Bern",
      "type": "Work",
      "long": 12.3,
      "lat": 56.7
    },
    "events": [
      {
        "eventUri": "<eventA>",
        "eventLabel": "Laura in Bern",
        "start": "1597-02-25",
        "end": "1597-08-11"
      },
      {
        "eventUri": "<eventB>",
        ...
      }
    ]
  },
  "<geoplace2>_Work": {...}, 
  "<geoplace2>_Voyage": {...}
}

For each key, the map displays a circle. The color depends on group.type, the radius on events.length. The above json example could also be expressed as an array of groups instead of an object with a key per group. I chose this approach, because it illustrates, that the groups identity is defined by pointUri and type.

Technical Specification

The component has these inputs: data: Parser.Binding[] (as in geov-yasgui-map-circles) colorScale: string[] (as in geov-yasgui-map-circles) timeFilterStart: string (in the form of xsd:date, setting the start of time-interva-filter on init)
timeFilterEnd: string (in the form of xsd:date, setting the end of time-interva-filter on init)

This example query can be copied it here to get useful sample data:

PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ontome: <https://ontome.net/ontology/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX time: <http://www.w3.org/2006/time#>

SELECT ?s ?event (SAMPLE(?l) as ?label) (SAMPLE(?lo) as ?long) (SAMPLE(?la) as ?lat)  (MIN(?date) as ?start) (MAX(?date) as ?end)
WHERE {

    # Geographical Place -had presence-> Presence -was at-> Place (lat/long)
    ?s ontome:p147i/ontome:p148 ?place.

    # Geographical Place -label-> label
    ?s rdfs:label ?l.

    # Geographical Place -is arrival place of-> Ship Voyage
    ?s ontome:p1335i ?event.

    # Event has time span 
    ?event ontome:p4 ?timespan.

    # time span -> time description -> year;month;day as xsd:date
    ?timespan ?timeprop ?dtd.
    ?dtd a time:DateTimeDescription ; time:year ?y; time:month ?m; time:day ?d .
    bind(xsd:date(concat(replace(str(?y), "^-", "" ), replace(str(?m), "^-", "" ),replace(str(?d), "^--", "" ))) as ?date)
    FILTER(isLiteral(?date)). # filter parsable records

    # Extract lat and long from WKT
    bind(replace(str(?place), '<http://www.opengis.net/def/crs/EPSG/0/4326>', "", "i") as ?rep)
    bind(xsd:float(replace(str(?rep), "^[^0-9\\.-]*([-]?[0-9\\.]+) .*$", "$1" )) as ?lo )
    bind(xsd:float(replace( str(?rep), "^.* ([-]?[0-9\\.]+)[^0-9\\.]*$", "$1" )) as ?la )

    # Append the project query param to the URI
    bind(concat(str(?s), "?p=84760") as ?link )
}
GROUP BY ?s ?event 
ORDER BY ?s

Acceptance Criteria

The user interface works as described in the functional requirements.

joschne commented 9 months ago

@flicksolutions please review specs

flicksolutions commented 8 months ago

@joschne

The map shows one circle per pointURI and type.

What does and type mean here? Does it mean the possibility of the pointURI being in the list twice but with a different type and then two points are being displayed on top of each other?

The size of a circle depends on the number of events

...at the same coordinates, right? It's not the case that all circles of EventA are the same size because there are 3 EventA in total. On the same point, this kind of implies, that the size of the circles implies that the size depends on the number of different Events:

one point for with radius 2 (for Laura and May) and color A (for Work)

If that is the case, please make it explicit.

I don't really get the difference between type and Event. Is a type something like a generalization of events? Isn't the fact that Maria worked in Lyon dependent on the Event? Why do we need a type?

On init, the map and the timeline zoom to the extend of the data

timeline shouldn't zoom right? The lowest value should just be the lowest value in the data and the max value should be the max value in the data and on init it should all be selected, right?

joschne commented 8 months ago

What does and type mean here? Does it mean the possibility of the pointURI being in the list twice but with a different type and then two points are being displayed on top of each other?

Exactly. In the example, <geoplace2> is displayed as two points, one per type.

joschne commented 8 months ago

The size of a circle depends on the number of events

...at the same coordinates, right? It's not the case that all circles of EventA are the same size because there are 3 EventA in total. On the same point, this kind of implies, that the size of the circles implies that the size depends on the number of different Events:

one point for with radius 2 (for Laura and May) and color A (for Work)

If that is the case, please make it explicit.

Would this text make the specs more understandable?

The map displays one circle per pointURI and type. This means, we group the events per pointURI and type. Each group will be displayed as a circle. The circle radius is proportional to the number of events per group, the color depends on the type.

More technically, each group of pointURI and type aggregates the event attributes in a list: start, end, eventLabel, eventURI and picks one pointLabel, lat, long for each group. The output data structure could look like this (pseudo code):

{
  "<geoplace1>_Work": {
    "group": {
      "pointURI": "<geoplace1>",
      "pointLabel": "Bern",
      "type": "Work",
      "long": 12.3,
      "lat": 56.7
    },
    "events": [
      {
        "eventUri": "<eventA>",
        "eventLabel": "Laura in Bern",
        "start": "1597-02-25",
        "end": "1597-08-11"
      },
      {
        "eventUri": "<eventB>",
        ...
      }
    ]
  },
  "<geoplace2>_Work": {...}, 
  "<geoplace2>_Voyage": {...}
}

For each key, the map displays a circle. The color depends on group.type, the radius on events.length. The above json example could also be expressed as an array of groups instead of an object with a key per group. I chose this approach, because it illustrates, that the groups identity is defined by pointUri and type.

joschne commented 8 months ago

timeline shouldn't zoom right? The lowest value should just be the lowest value in the data and the max value should be the max value in the data and on init it should all be selected, right?

correct!

flicksolutions commented 8 months ago

Yes, I now understand it better. Thanks! Could you update the description according to our discussion?

flicksolutions commented 8 months ago

I estimate 5 full days of work for this (complete with code review)