Статьи

Участок выборов Часть 1: Основы с Knockout.js, Bootstrap и d3.js

В этой статье показано, как вы можете использовать knockout.js, Twitter Bootstrap и d3.js вместе, чтобы создать приложение javascript, которое создает визуально приятную графику и диаграммы для ваших пользователей. Это полное клиентское решение, которое взаимодействует только с REST на сервере. Это первая часть серии статей, в которых обсуждаются эти концепции. В этой первой статье мы рассмотрим настройку базового приложения и продемонстрируем, как использовать knockout.js, bootstrap и d3.js. Это слишком много, чтобы показать весь javascript в этой статье. Вы можете найти демо для этой версии (с парой дополнительных функций) по этому адресу: http://www.smartjava.org/examples/election/part1.html . Поэтому, если вы хотите увидеть некоторые из утилит, которые я использую, посмотрите на javascript, используемый в этом примере.

Дело

В Нидерландах, где я живу, наши избранные лидеры склонны не выполнять свои условия. Чаще всего нет, из-за злоупотребления доверием или какой-то другой большой (или иногда незначительной) проблемы кабинет рухнул, и у нас новые выборы. Во время выборов нас спамят всевозможные прогнозы, диаграммы и люди, рассказывающие нам, как голландцы собираются голосовать. И когда мы наконец проголосуем, мы можем видеть обновления в режиме реального времени в течение всего вечера, и на многих сайтах, показывающих нам голосование за каждый муниципалитет.

Создание карты выборов

Эти обновления и то, как они представлены, довольно интересны. Обычно карта показывается со всеми муниципалитетами, где цвета показывают, как они проголосовали, затем докладчик увеличивает конкретный муниципалитет и показывает некоторые более подробные результаты. И это не то, что они делают только в Нидерландах:

Выборы в США

Выборы нас

Какие инструменты мы будем использовать?

Теперь трудно будет создать приложение, которое позволит вам просматривать результаты по районам, используя только стандартные веб-технологии. Нет Flash, нет плагинов, просто базовый HTML и JavaScript. В этой первой статье примера сайта выборов мы сосредоточимся на том, чтобы собрать вместе основные компоненты и работать. Чтобы быть более конкретным, мы создадим сайт, где мы используем следующие инструменты:

  • Knockout.js как наша прикладная среда javascript.
  • Twitter Bootstrap как основа для приложения и макета.
  • D3.js для облегчения работы с SVG и картами.

Помимо этих инструментов мы будем подключаться к сервису Play 2.0 Rest, который предоставляет нам информацию, которую мы покажем. В общем, мы будем создавать простое приложение, которое выглядит так:

Показать полную карту с цветами, представляющими конкретное значение

D3.js предоставил карту

Показать детали конкретного муниципалитета

Используйте загрузочный макет и таблицу

Пререквизиты: данные JSON

В этом примере мы будем использовать набор элементов geo json в качестве данных для нашего приложения. В предыдущей статье я уже показал основы того, как преобразовать данные в этот формат. Для получения дополнительной информации о том, как создать службу REST для предоставления этой информации, см. Мои другие статьи на эту тему . Для этого примера мы будем использовать набор записей GeoJSON. Эти записи содержат информацию о географии конкретного города, а также некоторые статистические данные о жителях (например, распределение по возрасту, процент иностранцев, вдовцов и т. Д.). Например, запись для Амстердама выглядит так:

{ "geometry" : { "coordinates" : [ [ [ [ 4.750558564252043,
                52.429385009319844
              ],
              [ 4.983615108367763,
                52.36497825292634
              ],
             ...
              [ 4.9846764177139704,
                52.36565814884794
              ]
            ] ]
        ],
      "type" : "MultiPolygon"
    },
  "gm_code" : "GM0363",
  "gm_naam" : "Amsterdam",
  "properties" : { "aant_inw" : "779810",
      "aant_man" : "383800",
      "aant_vrouw" : "396005",
      "aantal_hh" : "431060",
      "bev_dichth" : "4704",
      "gem_hh_gr" : "1.8",
      "gm_code" : "GM0363",
      "gm_naam" : "Amsterdam",
      "opp_land" : "16576",
      "opp_tot" : "21932",
      "opp_water" : "5356",
      "p_00_14_jr" : "16",
      "p_15_24_jr" : "13",
      "p_25_44_jr" : "35",
      "p_45_64_jr" : "25",
      "p_65_eo_jr" : "11",
      "p_ant_aru" : "2",
      "p_eenp_hh" : "56",
      "p_gehuwd" : "26",
      "p_gescheid" : "9",
      "p_hh_m_k" : "24",
      "p_hh_z_k" : "19",
      "p_marokko" : "9",
      "p_n_w_al" : "35",
      "p_ongehuwd" : "61",
      "p_over_nw" : "10",
      "p_surinam" : "9",
      "p_turkije" : "5",
      "p_verweduw" : "4",
      "p_west_al" : "15"
    },
  "type" : "Feature"
}

Knockout.js: настройка модели

Первое, что мы рассмотрим, это модель, которую мы будем использовать в Knockout.js. С Knockout.js вы можете привязать любой объект JavaScript к определенному элементу HTML / DOM. Итак, давайте посмотрим на модель, которую мы будем использовать для этого.

var country = function(cities, feature) {
    this.cities = cities;
    this.geojson = feature;
}
 
// city object containing the name, code and properties
// of a specific city. Also contains the geojson for
// this specific city.
var city = function(name, code, properties, feature)  {
    this.name = name;
    this.code = code;
    this.properties = properties;
    this.geojson = feature;
 
    this.getPropertyValue = function(propertyName) {
        for (var i = 0 ; i < properties.length ; i++) {
            if (properties[i].key == propertyName) return properties[i].value;
        }
    }
};

Очень простые объекты JavaScript. Может быть оптимизирован для производительности, но это пока не то. Последний объект модели, который нам нужен, это viemodel . Эта модель является центральной точкой доступа для привязок knockout.js и доступа из других библиотек javascript и сценариев.

var viewModelContainer = function() {
    self = this;
 
    // contains all the cities
    this.country = ko.observable();
 
    // the metrics that are available
    this.metrics = ko.observableArray([]);
 
    // the selected metric
    this.selectedMetric = ko.observable();
 
    // the selected city when we start is an empty variable
    this.selectedCity = ko.observable();
 
    // computed to change when either the metric
    // or the map changes
    this.metricChange = ko.computed(function() {
        this.selectedMetric();
        this.country();
    }, this);
 
    // function to retrieve a single city
    this.getCity = function(cityCode) {
        for(var i = 0 ; i < this.country().cities().length ; i++ ) {
            var city = this.country().cities()[i];
            if (city.code == cityCode) return city;
        }
    }
}

Здесь мы видим первые признаки knockout.js. Мы определяем количество объектов ko.observable (). Это означает, что knockout.js будет информировать объекты html / dom, связанные с таким ko.observable (), всякий раз, когда значение наблюдаемого изменяется. Когда мы посмотрим на HTML-часть этого урока, мы увидим, как их использовать. Во-первых, нам нужны данные для заполнения модели. Для этого мы используем поддержку REST JQuery:

Загрузка данных JSON с помощью вызова REST

Мы загружаем геоданные и различные метрики, используя следующий фрагмент кода.

function loadDataCBS() {
    // get all the fields
    $.getJSON(appConstants.JSON_URL_CBS, function(citiesList) {
        // get the cities to proces
        var allCities = citiesList.features;
 
        // temp value, before we push it to the observable array
        var cities = [];
 
        // keep track whether we analyzed the supplied metrics
        var propertiesAnalyzed = false;
        var metrics = [];
 
        // walk through the array and each city to the observable array
        for (i = 0 ; i < allCities.length ; i++) {
 
            // keys are a set of keypairs that are added to the object
            var keys = [];
            analyzeProperties(allCities[i].properties, keys, metrics, propertiesAnalyzed);
 
            // add all the found cities to the viewmodel at once. Add lazy loading in a
            // later stage
            cities.push(new city(
                allCities[i].gm_naam,
                allCities[i].gm_code,
                keys,
                allCities[i]
            ));
 
            // after the first iteration, metrics have been analyzed
            propertiesAnalyzed = true;
        }
 
        // create a country and setthe properties
        var country = new Country(cities,citiesList.features);
 
        // set the values in the viewmodel.
        viewModel.country(country);
        viewModel.metrics(metrics);
    })
}
 
function analyzeProperties(properties, keys, metrics, propertiesAnalyzed) {
    for(var propertyName in properties) {
        var prop = new property(propertyName,properties[propertyName]);
        keys.push(prop);
 
        if (!propertiesAnalyzed) {
            // simple analyze of the properties. If starts with p_ assume it is a percentage
            // which we can plot on the country overview
            if (prop.key.substring(0,2) == "p_") {
                metrics.push(prop.key);
            } else {
                // if doesn't start with a p_ we can check whether the value is
                // a number
                if (!isNaN(prop.value)==true) {
                    // we have number, so add this metric
                    metrics.push(prop.key);
                }
            }
        }
    }
}

Комментарии в строке должны объяснить, что происходит. По сути, мы делаем вызов, используя $ .getJSON (appConstants.JSON_URL_CBS, function (townsList) {, а затем анализируем результат и заполняем viewModel, чтобы у нас была информация для отображения в браузере.

Соедините все вещи

Нам нужно соединить все вместе, чтобы knockout.js знал, что связывать. Для этого мы создали простое «приложение», которое создает нашу модель, загружает данные json и инициализирует knockout.js.

function init() {
    // load json data
    loadDataCBS();
 
    // setup the projections, used to render geojson
    geo.setupGeo();
 
    // finally bind everything to the model
    ko.applyBindings(viewModel);
}
 
// create the viewmodel, this model can be referenced
// from all the different files
var viewModel = new viewModelContainer();
 
// and start the application
init();

Главная »Избирательный участок, часть 1: основы с Knockout.js, Bootstrap и d3.js

Участок выборов, часть 1: основы с Knockout.js, Bootstrap и d3.js

В этой статье показано, как вы можете использовать knockout.js, Twitter Bootstrap и d3.js вместе, чтобы создать приложение javascript, которое создает визуально приятную графику и диаграммы для ваших пользователей. Это полное клиентское решение, которое взаимодействует только с REST на сервере. Это первая часть серии статей, в которых обсуждаются эти концепции. В этой первой статье мы рассмотрим настройку базового приложения и продемонстрируем, как использовать knockout.js, bootstrap и d3.js. Это слишком много, чтобы показать весь javascript в этой статье. Вы можете найти демо для этой версии (с парой дополнительных функций) по этому адресу: http://www.smartjava.org/examples/election/part1.html . Поэтому, если вы хотите увидеть некоторые из утилит, которые я использую, посмотрите на javascript, используемый в этом примере.

Дело

В Нидерландах, где я живу, наши избранные лидеры склонны не выполнять свои условия. Чаще всего нет, из-за злоупотребления доверием или какой-то другой большой (или иногда незначительной) проблемы кабинет рухнул, и у нас новые выборы. Во время выборов нас спамят всевозможные прогнозы, диаграммы и люди, рассказывающие нам, как голландцы собираются голосовать. И когда мы наконец проголосуем, мы можем видеть обновления в режиме реального времени в течение всего вечера, и на многих сайтах, показывающих нам голосование за каждый муниципалитет.

Создание карты выборов

Эти обновления и то, как они представлены, довольно интересны. Обычно карта показывается со всеми муниципалитетами, где цвета показывают, как они проголосовали, затем докладчик увеличивает конкретный муниципалитет и показывает некоторые более подробные результаты. И это не то, что они делают только в Нидерландах:

Выборы в США

Выборы нас

Какие инструменты мы будем использовать?

Теперь трудно будет создать приложение, которое позволит вам просматривать результаты по районам, используя только стандартные веб-технологии. Нет Flash, нет плагинов, просто базовый HTML и JavaScript. В этой первой статье примера сайта выборов мы сосредоточимся на том, чтобы собрать вместе основные компоненты и работать. Чтобы быть более конкретным, мы создадим сайт, где мы используем следующие инструменты:

  • Knockout.js как наша прикладная среда javascript.
  • Twitter Bootstrap как основа для приложения и макета.
  • D3.js для облегчения работы с SVG и картами.

Помимо этих инструментов мы будем подключаться к сервису Play 2.0 Rest, который предоставляет нам информацию, которую мы покажем. В общем, мы будем создавать простое приложение, которое выглядит так:

Показать полную карту с цветами, представляющими конкретное значение

D3.js предоставил карту

Показать детали конкретного муниципалитета

Используйте загрузочный макет и таблицу

Пререквизиты: данные JSON

В этом примере мы будем использовать набор элементов geo json в качестве данных для нашего приложения. В предыдущей статье я уже показал основы того, как преобразовать данные в этот формат. Для получения дополнительной информации о том, как создать службу REST для предоставления этой информации, см. Мои другие статьи на эту тему . Для этого примера мы будем использовать набор записей GeoJSON. Эти записи содержат информацию о географии конкретного города, а также некоторые статистические данные о жителях (например, распределение по возрасту, процент иностранцев, вдовцов и т. Д.). Например, запись для Амстердама выглядит так:

{ "geometry" : { "coordinates" : [ [ [ [ 4.750558564252043,
                52.429385009319844
              ],
              [ 4.983615108367763,
                52.36497825292634
              ],
             ...
              [ 4.9846764177139704,
                52.36565814884794
              ]
            ] ]
        ],
      "type" : "MultiPolygon"
    },
  "gm_code" : "GM0363",
  "gm_naam" : "Amsterdam",
  "properties" : { "aant_inw" : "779810",
      "aant_man" : "383800",
      "aant_vrouw" : "396005",
      "aantal_hh" : "431060",
      "bev_dichth" : "4704",
      "gem_hh_gr" : "1.8",
      "gm_code" : "GM0363",
      "gm_naam" : "Amsterdam",
      "opp_land" : "16576",
      "opp_tot" : "21932",
      "opp_water" : "5356",
      "p_00_14_jr" : "16",
      "p_15_24_jr" : "13",
      "p_25_44_jr" : "35",
      "p_45_64_jr" : "25",
      "p_65_eo_jr" : "11",
      "p_ant_aru" : "2",
      "p_eenp_hh" : "56",
      "p_gehuwd" : "26",
      "p_gescheid" : "9",
      "p_hh_m_k" : "24",
      "p_hh_z_k" : "19",
      "p_marokko" : "9",
      "p_n_w_al" : "35",
      "p_ongehuwd" : "61",
      "p_over_nw" : "10",
      "p_surinam" : "9",
      "p_turkije" : "5",
      "p_verweduw" : "4",
      "p_west_al" : "15"
    },
  "type" : "Feature"
}

Knockout.js: настройка модели

Первое, что мы рассмотрим, это модель, которую мы будем использовать в Knockout.js. С Knockout.js вы можете привязать любой объект JavaScript к определенному элементу HTML / DOM. Итак, давайте посмотрим на модель, которую мы будем использовать для этого.

var country = function(cities, feature) {
    this.cities = cities;
    this.geojson = feature;
}
 
// city object containing the name, code and properties
// of a specific city. Also contains the geojson for
// this specific city.
var city = function(name, code, properties, feature)  {
    this.name = name;
    this.code = code;
    this.properties = properties;
    this.geojson = feature;
 
    this.getPropertyValue = function(propertyName) {
        for (var i = 0 ; i < properties.length ; i++) {
            if (properties[i].key == propertyName) return properties[i].value;
        }
    }
};

Очень простые объекты JavaScript. Может быть оптимизирован для производительности, но это пока не то. Последний объект модели, который нам нужен, это viemodel . Эта модель является центральной точкой доступа для привязок knockout.js и доступа из других библиотек javascript и сценариев.

var viewModelContainer = function() {
    self = this;
 
    // contains all the cities
    this.country = ko.observable();
 
    // the metrics that are available
    this.metrics = ko.observableArray([]);
 
    // the selected metric
    this.selectedMetric = ko.observable();
 
    // the selected city when we start is an empty variable
    this.selectedCity = ko.observable();
 
    // computed to change when either the metric
    // or the map changes
    this.metricChange = ko.computed(function() {
        this.selectedMetric();
        this.country();
    }, this);
 
    // function to retrieve a single city
    this.getCity = function(cityCode) {
        for(var i = 0 ; i < this.country().cities().length ; i++ ) {
            var city = this.country().cities()[i];
            if (city.code == cityCode) return city;
        }
    }
}

Здесь мы видим первые признаки knockout.js. Мы определяем количество объектов ko.observable (). Это означает, что knockout.js будет информировать объекты html / dom, связанные с таким ko.observable (), всякий раз, когда значение наблюдаемого изменяется. Когда мы посмотрим на HTML-часть этого урока, мы увидим, как их использовать. Во-первых, нам нужны данные для заполнения модели. Для этого мы используем поддержку REST JQuery:

Загрузка данных JSON с помощью вызова REST

Мы загружаем геоданные и различные метрики, используя следующий фрагмент кода.

function loadDataCBS() {
    // get all the fields
    $.getJSON(appConstants.JSON_URL_CBS, function(citiesList) {
        // get the cities to proces
        var allCities = citiesList.features;
 
        // temp value, before we push it to the observable array
        var cities = [];
 
        // keep track whether we analyzed the supplied metrics
        var propertiesAnalyzed = false;
        var metrics = [];
 
        // walk through the array and each city to the observable array
        for (i = 0 ; i < allCities.length ; i++) {
 
            // keys are a set of keypairs that are added to the object
            var keys = [];
            analyzeProperties(allCities[i].properties, keys, metrics, propertiesAnalyzed);
 
            // add all the found cities to the viewmodel at once. Add lazy loading in a
            // later stage
            cities.push(new city(
                allCities[i].gm_naam,
                allCities[i].gm_code,
                keys,
                allCities[i]
            ));
 
            // after the first iteration, metrics have been analyzed
            propertiesAnalyzed = true;
        }
 
        // create a country and setthe properties
        var country = new Country(cities,citiesList.features);
 
        // set the values in the viewmodel.
        viewModel.country(country);
        viewModel.metrics(metrics);
    })
}
 
function analyzeProperties(properties, keys, metrics, propertiesAnalyzed) {
    for(var propertyName in properties) {
        var prop = new property(propertyName,properties[propertyName]);
        keys.push(prop);
 
        if (!propertiesAnalyzed) {
            // simple analyze of the properties. If starts with p_ assume it is a percentage
            // which we can plot on the country overview
            if (prop.key.substring(0,2) == "p_") {
                metrics.push(prop.key);
            } else {
                // if doesn't start with a p_ we can check whether the value is
                // a number
                if (!isNaN(prop.value)==true) {
                    // we have number, so add this metric
                    metrics.push(prop.key);
                }
            }
        }
    }
}

Комментарии в строке должны объяснить, что происходит. По сути, мы делаем вызов, используя $ .getJSON (appConstants.JSON_URL_CBS, function (townsList) {, а затем анализируем результат и заполняем viewModel, чтобы у нас была информация для отображения в браузере.

Соедините все вещи

Нам нужно соединить все вместе, чтобы knockout.js знал, что связывать. Для этого мы создали простое «приложение», которое создает нашу модель, загружает данные json и инициализирует knockout.js.

function init() {
    // load json data
    loadDataCBS();
 
    // setup the projections, used to render geojson
    geo.setupGeo();
 
    // finally bind everything to the model
    ko.applyBindings(viewModel);
}
 
// create the viewmodel, this model can be referenced
// from all the different files
var viewModel = new viewModelContainer();
 
// and start the application
init();

В нашем html мы включаем этот javascript в конце, поэтому после загрузки html приложение инициализируется.

Привязать элементы HTML к модели

В этой первой части мы не собираемся создавать слишком сложные привязки. В основном нам нужно связать следующие элементы:

  • Выпадающие окна с выбором
  • Таблица, показывающая свойства выбранного города
  • Карта SVG, чтобы показать детали выбранного показателя
  • Карта SVG для отображения контуров выбранного города

Привязать выпадающие списки

Мы хотим связать информацию из модели (все доступные города и все доступные метрики) с двумя выпадающими списками.
привязать выбор к нокауту
Мы делаем это напрямую через привязку knockout.js:

<div data-bind="with: country">
                    <select data-bind="options: cities,
                       optionsText: 'name',
                       value: $parent.selectedCity,
                       optionsCaption: 'Choose city'"></select>
                </div>
 
                <!--
                    Bind to list of available metrics. Only do this, when
                    no city has been selected.
                -->
                <div data-bind="ifnot: selectedCity">
                    <select data-bind="options: metrics,
                       value: selectedMetric,
                       optionsCaption: 'Choose metric'"></select>
                </div>

В этом примере вы можете увидеть несколько интересных функций knockout.js. Давайте начнем с рассмотрения второй привязки. Здесь мы используем data-bind = «ifnot: selectedCity», чтобы определить, следует ли отображать этот конкретный div. Если ни один город не выбран, этот div отображается и привязка элемента select обрабатывается. В этой привязке мы привязываем «параметры» элемента select к свойству metrics нашей viemodel, и после выбора значения оно сохраняется в свойстве selectedMetric модели представления.
Для первого выпадающего списка мы делаем нечто подобное, но мы также обернули div внутри другого div с помощью data-bind = «with: country»связывание. Это означает, что все привязки внутри этого div обрабатываются в контексте элемента «страна». Привязка выпадающих точек к «городам», так как мы находимся в контексте элемента страны, все элементы из массива country.cities показаны в этом раскрывающемся списке. Метка, которую мы показываем, указывает на свойство объекта city: name, и когда город выбран, он сохраняется в «$ parent.selectedCity». $ parent — это свойство, которое вы можете использовать для доступа к родительскому контексту. В этом случае, поскольку мы используем привязку «с», мы можем получить доступ к корневому контексту модели представления, чтобы установить свойство selectedCity.

Показать таблицу, когда выбран город

Когда пользователь выбирает город из выпадающего меню, мы хотим показать таблицу со всеми свойствами этого города. Для этого мы используем следующий фрагмент HTML

<div data-bind="if: selectedCity" class="span9">
                    <div class="well">
                        <h2>Properties</h2>
                        You have chosen a city with the following properties:
                        <table class="table">
                            <thead>
                            <tr>
                                <th>Property</th>
                                <th>Value</th>
                            </tr>
                            </thead>
                            <!-- iterate over each property, and add a row -->
                            <tbody data-bind="foreach: selectedCity().properties">
                            <tr>
                                <td data-bind="text: $data.key"></td>
                                <td data-bind="text: $data.value"></td>
                            </tr>
                            </tbody>
                        </table>
                    </div>
                </div>

Мы еще раз используем привязку, чтобы увидеть, следует ли отображать этот div, так же, как мы показали ранее. Если выбран город, мы начинаем отображать таблицу и привязываем элемент «tbody» к списку свойств с привязкой «foreach». Как следует из названия, эта привязка перебирает свойства, чтобы отобразить элементы «tr». Затем нам просто нужно сделать простую привязку текста к «$ data.key», чтобы отобразить содержимое ячеек. Здесь $ data — это специальное ключевое слово, которое позволяет вам получить доступ к текущему свойству из списка, который обрабатывается.
таблица с использованием knockout.js

Карта SVG для отображения контуров выбранного города

До сих пор мы видели, как соединить выпадающие списки и таблицу с моделью вида нокаута. Если город, который мы выбрали, изменяется, наша модель просмотра обновляется, что также вызывает обновление таблицы. Следующим компонентом, который мы хотим показать при выборе города, являются его контуры. Когда мы получили основной объект JSON, мы также получили географические координаты (в формате WGS84) для каждого города, которые мы сохранили в нашей модели:

var city = function(name, code, properties, feature)  {
    this.name = name;
    this.code = code;
    this.properties = properties;
    this.geojson = feature;  // the contours of the city
 
    this.getPropertyValue = function(propertyName) {
        for (var i = 0 ; i < properties.length ; i++) {
            if (properties[i].key == propertyName) return properties[i].value;
        }
    }
};

Чтобы отобразить это как карту, мы используем библиотеку excellend d3.js , я не буду вдаваться в подробности (см. Эту статью для получения дополнительной информации) о d3.js, я просто покажу, как вы можете создать собственную привязку, которая возвращает визуализированную карту. Цель, к которой мы стремимся, заключается в следующем:

роттердам-SVG-d3.js

Для этого мы создадим пользовательскую привязку, которую мы будем использовать так:

  <div data-bind="if: selectedCity" class="span9">
                    <div class="well">
                        <h2>View of the city <span
                                data-bind="text: selectedCity() ? selectedCity().name : 'unknown'"></span></h2>
                        <!--
                            in this binding, we bind directly to the group element,
                            whenever the selectedCity changes, we update this element using
                            a custom binding.
                        -->
                        <svg id="localMapContainer" xmlns="http://www.w3.org/2000/svg">
                            <g id="localMap"
                               data-bind="d3svgSingle: selectedCity"/>
                        </svg>
                    </div>

Как видите, мы сначала используем обычную привязку knockout.js, чтобы связать имя с нашим заголовком. И затем мы привязываем элемент группы svg к пользовательской привязке d3svgSingle. Это означает, что эта привязка будет вызываться каждый раз, когда наш selectedCity в модели изменяется. Это пользовательское связывание может быть добавлено с использованием этого фрагмента JavaScript:

ko.bindingHandlers.d3svgSingle = {
    init:function (element, valueAccessor, allBindingsAccessor, viewModel) {
        // we don't do anything on initialize
    },
    update:function (element, valueAccessor, allBindingsAccessor, viewModel) {
        // make sure the model is correctly initialized. If not don't render
        // anything
        if (typeof viewModel.selectedCity() != "undefined" && element.id != null) {
            // first remove all the elements currently set on the object
            // for this we use the d3 helper function. Remember for the single
            // city we render directly to the group.
            d3.select(element).selectAll("path").remove();
 
            // now convert the geojson string to a path object
            // and add it to the supplied binding
            var svgPath = geo.path(viewModel.selectedCity().geojson);
            d3.select(element)
                .append("path")
                .attr("d", svgPath);
 
            // resize to fill the complete div
            geons.resizeSVG(element, appConstants.CITY_VIEW_X, appConstants.CITY_VIEW_Y);
        }
    }
}

Встроенные комментарии должны хорошо объяснить, что мы здесь делаем. Мы используем функцию d3.js geo.path (инициализация геообъекта здесь не показана) для преобразования информации о геоджонсе в путь SVG. Затем мы используем d3.js, чтобы добавить элемент «путь» в группу, к которой мы привязали свойство, и в этом новом элементе пути мы устанавливаем атрибут d со значением пути svg. Последний шаг, который мы здесь делаем, это изменение размера svg, чтобы он хорошо соответствовал пространству, которое мы для него выделили (код не показан).

Карта SVG, чтобы показать детали выбранного показателя

Последний шаг, который нам нужно сделать, это показать карту, которая представляет значения выбранной метрики:

карта

Это также называется карта ac choropleth . Для этого мы еще раз создаем пользовательское связывание, которое мы связываем следующим образом:

 <!--If no city is selected, view the complete map-->
                <div data-bind="ifnot: selectedCity" class="span9">
                    <div class="well">
 
 
                            <h2>Statistics</h2>
 
                        <!-- here we bind the svg element itself to a binding. If the
                             country value changes, this element is updated -->
                        <svg id="countryMapContainer" xmlns="http://www.w3.org/2000/svg"
                             data-bind="d3svg: metricChange()">
                        </svg>
                    </div>
                </div>
            </div>

Этот div отображается, когда город не выбран. Содержимое svg обновляется всякий раз, когда изменяется одна из метрик. Прежде чем мы рассмотрим код привязки, давайте посмотрим, как это настроено в нашей модели представления.

  // computed to change when either the metric
    // or the map changes
    this.metricChange = ko.computed(function() {
        this.selectedMetric();
        this.country();
    }, this);

Как видите, это значение metricChange — это не обычная привязка knockout.js, а вычисленная. Другими словами, наше представление будет обновляться всякий раз, когда изменяется элемент страны (например, когда загружается модель) или когда изменяется метрика (раскрывающийся список). Таким образом, мы можем легко позволить представлению реагировать на изменения в нескольких элементах нашего представления. Сам код привязки снова использует d3.js для рендеринга карты.

// custom handler to draw the complete map
ko.bindingHandlers.d3svg = {
    init:function (element, valueAccessor, allBindingsAccessor, viewModel) {
        // we don't do anything on initialize
    },
    update:function (element, valueAccessor, allBindingsAccessor, viewModel) {
        // if we've got a filled json object with geo information
        // we use it to drawn the complete country
        if (typeof viewModel.country() != "undefined" &&
            typeof viewModel.selectedCity() == "undefined") {
 
            // remove everything, for the total map, we render directly
            // to a svg element.
            d3.select(element).selectAll("g").remove();
 
            // based on whether we've got a selected metric, we run a quantize function
            // on the values. To do this we first need to calculate the max and min
            // values. We also store the metric values directly in a map for easy reference
            var qMin, qMax, metrics = {};
            if (viewModel.selectedMetric() != null) {
                console.log(viewModel.selectedMetric());
                for (i = 0; i < viewModel.country().cities.length; i++) {
                    var metricValue = viewModel.country().cities[i].getPropertyValue(viewModel.selectedMetric());
                    var parsed = parseInt(metricValue);
                    if (i == 0) {
                        qMin = parsed; qMax = parsed;
                    } else {
                        if (parsed < qMin && parsed >= 0) qMin = parsed;
                        if (parsed > qMax) qMax = parsed;
                    }
                    metrics[viewModel.country().cities[i].code] = parsed;
                }
            }
 
            // draw the map, and color the fields based on the metrics
            drawMap(element, metrics, qMax, qMin);
 
 
            // if we've got a metric selected, render a legend
            if (typeof viewModel.selectedMetric() != "undefined" || viewModel.election().selectedParty() != null) {
                // draw the legend
                drawLegend(element, qMin, qMax)
            }
        }
    }
};

В этой части привязки мы проверяем, являются ли значения такими, как мы ожидали, и, если так, определяем минимальное и максимальное значения нашей метрики. Мы делаем это, чтобы определить, как нарисовать легенду и как распределить значения по этим цветам. Фактическое рисование карты и легенды выполняется в функциях drawMap и drawLegend.

function drawMap(element, metrics, qMax, qMin) {
 
    d3.select(element)
        .append("g")
        .attr("id", "countryMap")
        .attr("class", appConstants.COLORBASE)
        .selectAll("path")
        .data(viewModel.country().geojson)
        .enter().append("path")
        .attr("d", geo.path)
        .attr("class", function (d) {
 
            if (viewModel.selectedMetric() != null) {
 
                var metricValue = metrics[d.gm_code];
                // we have a range of qMin to qMax, which should be divided
                // into 8 equal steps.
                // (value - qMin + qMax) / 8
                var range = qMax - qMin;
                var calcValue = metricValue - qMin;
 
                return "q" + Math.min(8, ~~(calcValue / (range / 8))) + "-9";
            }
        })
}

Это код d3.js, который отображает карту на основе геойсона. Элемент класса пути svg, который мы добавляем, контуры одного города, определяется функцией, которую мы предоставляем. Эта функция распределяет значение выбранной метрики на 8 частей. Таким образом, в зависимости от значения контуру присваивается класс в виде q2-9 или q3-9 и т. Д. Имена значений требуемых классов предоставляются файлом CSS ColorBrewer . Этот файл содержит диапазоны цветов, которые вы можете использовать для раскрашивания карт (или других графиков):

.PuBu .q0-8{fill:rgb(255,247,251)}
.PuBu .q1-8{fill:rgb(236,231,242)}
.PuBu .q2-8{fill:rgb(208,209,230)}
.PuBu .q3-8{fill:rgb(166,189,219)}
.PuBu .q4-8{fill:rgb(116,169,207)}
.PuBu .q5-8{fill:rgb(54,144,192)}
.PuBu .q6-8{fill:rgb(5,112,176)}
.PuBu .q7-8{fill:rgb(3,78,123)}

DrawLegend не очень интересный метод. Это просто использует основные конструкции SVG, чтобы нарисовать простую легенду:

// draw a legend
function drawLegend(element, qMin, qMax) {
    // create a new group with the specific base color and add the lower value
    d3.select(element)
        .append("g")
            .attr("id", "legenda").attr("class", appConstants.COLORBASE)
        .append("text")
            .attr("x", "20").attr("y", "40").text("Min: " + Math.round(qMin*100)/100);
 
    // add the various blocks of the legenda
    d3.select(element).select("#legenda").selectAll("rect")
        .data(d3.range(0, 8))
        .enter()
        .append("rect")
            .attr("width", "20").attr("height", "20").attr("y", "0")
            .attr("class", function (d, i) {
                return "q" + i + "-9";
            })
            .attr("x", function (d, i) {
                return (i + 1) * 20;
             });
 
    // add a text element
    d3.select(element).select("#legenda").append("text")
        .attr("x", "140").attr("y", "40").text("Max: " + Math.round(qMax*100)/100)
}

С этой последней легендой мы обсудили все различные части нашего первого приложения. Мы показали роль, которую играет в этом knockout.js, и использовали d3.js для рендеринга элементов svg. Последнее, что мы сделаем в этом уроке, это настройка базовой схемы начальной загрузки . Bootstrap — это проект из Twitter, который позволяет вам легко создавать сетки и гибкие макеты на основе CSS. Для этого примера я использовал следующий макет (полный index.html без привязок):

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Dutch Elections: Step 1</title>
    <!-- load externally used libraries -->
    <script type='text/javascript' src='libraries/jquery/jquery-1.7.2.js'></script>
    <script type='text/javascript' src='libraries/d3/d3.js'></script>
    <script type='text/javascript' src='libraries/d3/d3.geo.js'></script>
    <script type='text/javascript' src='libraries/knockout/knockout-2.0.0.js'></script>
 
    <!-- load boot strap styles -->
    <link href="libraries/bootstrap/assets/css/bootstrap.css" rel="stylesheet">
    <link href="libraries/bootstrap/assets/css/bootstrap-responsive.css" rel="stylesheet">
 
    <!-- load own styles -->
    <link href="css/style.css" rel="stylesheet">
 
    <!-- load colorbrewer styles -->
    <link href="css/colorbrewer.css" rel="stylesheet">
 
    <!-- From bootstrap, support for IE6-8 support of HTML5 elements -->
    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
</head>
 
<body>
 
<!-- use the bootstap fluid container -->
<div class="container-fluid">
    <div class="well-small">
        <h1>Election using Knockout.js, Bootstrap and d3.js</h1>
    </div>
 
    <div class="row-fluid">
        <!-- left column, contains the select options -->
        <div class="span3">
            <div class="well">
 
                <!--
                    Bind to cities view model. Show the name of the city element. Bind
                    the selected element to selectedCity. Has default element with value
                    'Choose...'
                -->
                <div data-bind="with: country">
                    ...
                </div>
 
                <!--
                    Bind to list of available metrics. Only do this, when
                    no city has been selected.
                -->
                <div data-bind="ifnot: selectedCity">
                 ...
                </div>
            </div>
        </div>
 
 
        <!-- right column shows the information -->
        <div class="span9">
            <div class="row">
                <!--If a city is selected, view the city-->
                <div data-bind="if: selectedCity" class="span9">
                    <div class="well">
                        ...
                    </div>
                </div>
 
                <div data-bind="ifnot: selectedCity" class="span9">
                    <div class="well">
                   ...
                    </div>
                </div>
            </div>
 
            <div class="row">
                <div data-bind="if: selectedCity" class="span9">
                    <div class="well">
                    ...
                    </div>
                </div>
            </div>
 
            <div class="row">
                <!--
                    this element is processed if a city is selected. This shows all
                    the properties of the selected city in a table
                -->
                <div data-bind="if: selectedCity" class="span9">
                    <div class="well">
                     ...
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
 
 
<!-- after the html is loaded, load the application and bind
     the variables. -->
<script type='text/javascript' src='js/app/election-constants.js'></script>
<script type='text/javascript' src='js/models/city.js'></script>
<script type='text/javascript' src='js/app/util-geo.js'></script>
<script type='text/javascript' src='js/app/util-json.js'></script>
<script type='text/javascript' src='js/app/election-bindings.js'></script>
<script type='text/javascript' src='js/app/election.js'></script>
 
<!-- finally load all the bootstrap assets -->
<script src="libraries/bootstrap/assets/js/jquery.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-transition.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-alert.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-modal.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-dropdown.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-scrollspy.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-tab.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-tooltip.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-popover.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-button.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-collapse.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-carousel.js"></script>
<script src="libraries/bootstrap/assets/js/bootstrap-typeahead.js"></script>
</body>
</html>

Я пока не буду вдаваться в подробности начальной загрузки. Для этого примера мы использовали только небольшую часть того, что предлагает начальная загрузка. Единственное, что мы использовали:

  • хорошо маленький класс: красиво визуализировать выпадающие окна
  • класс жидкости в ряду: для двухколоночной схемы Левый столбец, содержит выпадающие окна. Правый столбец информации
  • span # class: определить размер определенного столбца и строки
  • well class: красиво отображать содержимое в серой области
  • row row: отображает строку внутри span # div

В следующих статьях мы уделим больше времени возможностям, которые нам предлагает бутстрап. Если вам нужна дополнительная информация об используемых элементах, посмотрите раздел макета начальной загрузки: http://twitter.github.com/bootstrap/scaffolding.html#gridSystem

Вот именно для этого урока. В следующем уроке мы представим фактические результаты выборов и используем начальную загрузку для определения макета, который мы будем использовать. Мы также расширим knockout.js с Sammy.js для обработки отображения URL для поддержки кнопок назад / вперед и покажем вам, как вы можете использовать d3.js для рендеринга графиков и отображения информации интересными способами. Если вы хотите просмотреть весь исходный код, откройте http://www.smartjava.org/examples/election/part1.html в своем браузере и посмотрите на исходный код.